From abef6da56913f1c55528103e60a50451a39628b1 Mon Sep 17 00:00:00 2001 From: WlodekM Date: Sun, 16 Jun 2024 10:35:45 +0300 Subject: initial commit --- src/Animations.c | 409 +++ src/Animations.h | 10 + src/Audio.c | 566 +++ src/Audio.h | 90 + src/AudioBackend.c | 1717 +++++++++ src/AxisLinesRenderer.c | 95 + src/AxisLinesRenderer.h | 14 + src/Bitmap.c | 730 ++++ src/Bitmap.h | 104 + src/Block.c | 805 +++++ src/Block.h | 154 + src/BlockID.h | 91 + src/BlockPhysics.c | 573 +++ src/BlockPhysics.h | 29 + src/Builder.c | 1662 +++++++++ src/Builder.h | 24 + src/Camera.c | 355 ++ src/Camera.h | 85 + src/Chat.c | 311 ++ src/Chat.h | 74 + src/Commands.c | 742 ++++ src/Commands.h | 31 + src/Constants.h | 78 + src/Core.h | 494 +++ src/Deflate.c | 1317 +++++++ src/Deflate.h | 137 + src/Drawer.c | 137 + src/Drawer.h | 38 + src/Drawer2D.c | 714 ++++ src/Drawer2D.h | 129 + src/Entity.c | 1107 ++++++ src/Entity.h | 260 ++ src/EntityComponents.c | 1152 ++++++ src/EntityComponents.h | 135 + src/EntityRenderers.c | 478 +++ src/EntityRenderers.h | 21 + src/EnvRenderer.c | 958 +++++ src/EnvRenderer.h | 47 + src/Errors.h | 141 + src/Event.c | 201 ++ src/Event.h | 222 ++ src/ExtMath.c | 468 +++ src/ExtMath.h | 78 + src/FancyLighting.c | 580 +++ src/Formats.c | 1897 ++++++++++ src/Formats.h | 39 + src/Funcs.h | 52 + src/Game.c | 796 +++++ src/Game.h | 157 + src/GameVersion.c | 91 + src/Generator.c | 817 +++++ src/Generator.h | 44 + src/Graphics.h | 309 ++ src/Graphics_3DS.c | 1056 ++++++ src/Graphics_D3D11.c | 1209 +++++++ src/Graphics_D3D9.c | 885 +++++ src/Graphics_Dreamcast.c | 643 ++++ src/Graphics_GCWii.c | 598 ++++ src/Graphics_GL1.c | 691 ++++ src/Graphics_GL2.c | 661 ++++ src/Graphics_N64.c | 491 +++ src/Graphics_NDS.c | 517 +++ src/Graphics_PS1.c | 878 +++++ src/Graphics_PS2.c | 666 ++++ src/Graphics_PS3.c | 703 ++++ src/Graphics_PSP.c | 450 +++ src/Graphics_PSVita.c | 1113 ++++++ src/Graphics_Saturn.c | 565 +++ src/Graphics_SoftGPU.c | 674 ++++ src/Graphics_WiiU.c | 481 +++ src/Graphics_Xbox.c | 701 ++++ src/Graphics_Xbox360.c | 407 +++ src/Gui.c | 731 ++++ src/Gui.h | 306 ++ src/HeldBlockRenderer.c | 267 ++ src/HeldBlockRenderer.h | 15 + src/Http.h | 90 + src/Http_Web.c | 148 + src/Http_Worker.c | 1387 ++++++++ src/Input.c | 1467 ++++++++ src/Input.h | 272 ++ src/Inventory.c | 154 + src/Inventory.h | 63 + src/IsometricDrawer.c | 170 + src/IsometricDrawer.h | 20 + src/LBackend.c | 1179 +++++++ src/LBackend.h | 76 + src/LBackend_Android.c | 614 ++++ src/LScreens.c | 1856 ++++++++++ src/LScreens.h | 52 + src/LWeb.c | 728 ++++ src/LWeb.h | 136 + src/LWidgets.c | 793 +++++ src/LWidgets.h | 251 ++ src/Launcher.c | 583 ++++ src/Launcher.h | 81 + src/Lighting.c | 475 +++ src/Lighting.h | 88 + src/Logger.c | 1386 ++++++++ src/Logger.h | 58 + src/MapRenderer.c | 809 +++++ src/MapRenderer.h | 78 + src/Menus.c | 4042 +++++++++++++++++++++ src/Menus.h | 69 + src/Model.c | 2439 +++++++++++++ src/Model.h | 300 ++ src/Options.c | 227 ++ src/Options.h | 158 + src/PackedCol.c | 95 + src/PackedCol.h | 64 + src/Particle.c | 619 ++++ src/Particle.h | 46 + src/Physics.c | 244 ++ src/Physics.h | 41 + src/Picking.c | 261 ++ src/Picking.h | 45 + src/Platform.h | 340 ++ src/Platform_3DS.c | 457 +++ src/Platform_Android.c | 253 ++ src/Platform_BeOS.cpp | 110 + src/Platform_Dreamcast.c | 537 +++ src/Platform_GCWii.c | 619 ++++ src/Platform_MacClassic.c | 544 +++ src/Platform_N64.c | 298 ++ src/Platform_NDS.c | 501 +++ src/Platform_PS1.c | 250 ++ src/Platform_PS2.c | 763 ++++ src/Platform_PS3.c | 490 +++ src/Platform_PSP.c | 476 +++ src/Platform_PSVita.c | 432 +++ src/Platform_Posix.c | 1578 +++++++++ src/Platform_Saturn.c | 240 ++ src/Platform_Switch.c | 538 +++ src/Platform_Web.c | 438 +++ src/Platform_WiiU.c | 463 +++ src/Platform_Windows.c | 1135 ++++++ src/Platform_Xbox.c | 453 +++ src/Platform_Xbox360.c | 295 ++ src/Protocol.c | 1930 ++++++++++ src/Protocol.h | 75 + src/Queue.c | 72 + src/Queue.h | 22 + src/Resources.c | 1269 +++++++ src/Resources.h | 37 + src/SSL.c | 582 +++ src/SSL.h | 16 + src/Screens.c | 2199 ++++++++++++ src/Screens.h | 46 + src/SelOutlineRenderer.c | 141 + src/SelOutlineRenderer.h | 12 + src/SelectionBox.c | 233 ++ src/SelectionBox.h | 16 + src/Server.c | 555 +++ src/Server.h | 74 + src/Stream.c | 499 +++ src/Stream.h | 101 + src/String.c | 1027 ++++++ src/String.h | 273 ++ src/SystemFonts.c | 1111 ++++++ src/SystemFonts.h | 41 + src/TexturePack.c | 667 ++++ src/TexturePack.h | 115 + src/TouchUI.c | 798 +++++ src/Utils.c | 362 ++ src/Utils.h | 77 + src/Vectors.c | 260 ++ src/Vectors.h | 137 + src/VirtualKeyboard.h | 367 ++ src/Vorbis.c | 1674 +++++++++ src/Vorbis.h | 69 + src/Widgets.c | 2821 +++++++++++++++ src/Widgets.h | 319 ++ src/Window.h | 246 ++ src/Window_3DS.c | 293 ++ src/Window_Android.c | 561 +++ src/Window_BeOS.cpp | 743 ++++ src/Window_Dreamcast.c | 353 ++ src/Window_GCWii.c | 554 +++ src/Window_MacClassic.c | 558 +++ src/Window_N64.c | 197 ++ src/Window_NDS.c | 350 ++ src/Window_PS1.c | 223 ++ src/Window_PS2.c | 301 ++ src/Window_PS3.c | 394 +++ src/Window_PSP.c | 196 ++ src/Window_PSVita.c | 345 ++ src/Window_SDL.c | 509 +++ src/Window_SDL3.c | 541 +++ src/Window_Saturn.c | 246 ++ src/Window_Switch.c | 309 ++ src/Window_Terminal.c | 645 ++++ src/Window_Web.c | 810 +++++ src/Window_WiiU.cpp | 610 ++++ src/Window_Win.c | 865 +++++ src/Window_X11.c | 1459 ++++++++ src/Window_Xbox.c | 254 ++ src/Window_Xbox360.c | 217 ++ src/Window_cocoa.m | 961 +++++ src/World.c | 326 ++ src/World.h | 217 ++ src/_GLShared.h | 338 ++ src/_GraphicsBase.h | 559 +++ src/_HttpBase.h | 313 ++ src/_PlatformBase.h | 175 + src/_PlatformConsole.h | 195 ++ src/_WindowBase.h | 215 ++ src/_autofit.c | 36 + src/_cff.c | 31 + src/_ftbase.c | 36 + src/_ftbitmap.c | 812 +++++ src/_ftglyph.c | 553 +++ src/_ftinit.c | 105 + src/_ftsynth.c | 123 + src/_psaux.c | 41 + src/_pshinter.c | 29 + src/_psmodule.c | 619 ++++ src/_sfnt.c | 32 + src/_smooth.c | 27 + src/_truetype.c | 30 + src/_type1.c | 30 + src/freetype/README (2) | 532 +++ src/freetype/afangles.c | 285 ++ src/freetype/afangles.h | 7 + src/freetype/afblue.c | 688 ++++ src/freetype/afblue.h | 389 +++ src/freetype/afcover.h | 105 + src/freetype/afdummy.c | 75 + src/freetype/afdummy.h | 40 + src/freetype/aferrors.h | 42 + src/freetype/afglobal.c | 434 +++ src/freetype/afglobal.h | 169 + src/freetype/afhints.c | 1227 +++++++ src/freetype/afhints.h | 481 +++ src/freetype/aflatin.c | 3572 +++++++++++++++++++ src/freetype/aflatin.h | 194 + src/freetype/afloader.c | 716 ++++ src/freetype/afloader.h | 91 + src/freetype/afmodule.c | 144 + src/freetype/afmodule.h | 58 + src/freetype/afranges.c | 908 +++++ src/freetype/afranges.h | 47 + src/freetype/afscript.h | 351 ++ src/freetype/afshaper.c | 131 + src/freetype/afshaper.h | 63 + src/freetype/afstyles.h | 445 +++ src/freetype/aftypes.h | 550 +++ src/freetype/afwarp.c | 373 ++ src/freetype/afwarp.h | 64 + src/freetype/afwrtsys.h | 48 + src/freetype/autohint.h | 220 ++ src/freetype/cffcmap.c | 217 ++ src/freetype/cffcmap.h | 67 + src/freetype/cffdecode.c | 239 ++ src/freetype/cffdecode.h | 64 + src/freetype/cffdrivr.c | 491 +++ src/freetype/cffdrivr.h | 38 + src/freetype/cfferrs.h | 42 + src/freetype/cffgload.c | 683 ++++ src/freetype/cffgload.h | 63 + src/freetype/cffload.c | 2564 ++++++++++++++ src/freetype/cffload.h | 113 + src/freetype/cffobjs.c | 1114 ++++++ src/freetype/cffobjs.h | 85 + src/freetype/cffotypes.h | 108 + src/freetype/cffparse.c | 1512 ++++++++ src/freetype/cffparse.h | 136 + src/freetype/cfftoken.h | 150 + src/freetype/cfftypes.h | 412 +++ src/freetype/fnterrs.h | 42 + src/freetype/freetype.h | 3499 +++++++++++++++++++ src/freetype/ft2build.h | 42 + src/freetype/ftadvanc.c | 175 + src/freetype/ftadvanc.h | 181 + src/freetype/ftapi.c | 121 + src/freetype/ftbase.h | 65 + src/freetype/ftbitmap.h | 226 ++ src/freetype/ftcalc.c | 972 ++++++ src/freetype/ftcalc.h | 444 +++ src/freetype/ftconfig.h | 570 +++ src/freetype/ftdebug.h | 251 ++ src/freetype/ftdriver.h | 1153 ++++++ src/freetype/ftdrv.h | 400 +++ src/freetype/fterrdef.h | 280 ++ src/freetype/fterrors.h | 226 ++ src/freetype/ftfntfmt.c | 40 + src/freetype/ftfntfmt.h | 80 + src/freetype/ftgloadr.c | 364 ++ src/freetype/ftgloadr.h | 154 + src/freetype/ftglyph.h | 550 +++ src/freetype/ftgrays.c | 1377 ++++++++ src/freetype/ftgrays.h | 58 + src/freetype/fthash.c | 224 ++ src/freetype/fthash.h | 100 + src/freetype/ftheader.h | 636 ++++ src/freetype/ftimage.h | 973 ++++++ src/freetype/ftlist.h | 187 + src/freetype/ftmac.c | 426 +++ src/freetype/ftmac.h | 115 + src/freetype/ftmemory.h | 393 +++ src/freetype/ftmisc.h | 142 + src/freetype/ftmodapi.h | 361 ++ src/freetype/ftmoderr.h | 194 + src/freetype/ftmodule.h | 23 + src/freetype/ftobjs.c | 3298 +++++++++++++++++ src/freetype/ftobjs.h | 1025 ++++++ src/freetype/ftoption.h | 728 ++++ src/freetype/ftoutln.c | 860 +++++ src/freetype/ftoutln.h | 552 +++ src/freetype/ftparams.h | 119 + src/freetype/ftrender.h | 123 + src/freetype/ftserv.h | 1011 ++++++ src/freetype/ftsizes.h | 153 + src/freetype/ftsmerrs.h | 42 + src/freetype/ftsmooth.c | 162 + src/freetype/ftsmooth.h | 38 + src/freetype/ftstdlib.h | 144 + src/freetype/ftstream.c | 695 ++++ src/freetype/ftstream.h | 485 +++ src/freetype/ftsynth.h | 74 + src/freetype/ftsystem.h | 355 ++ src/freetype/fttrace.h | 110 + src/freetype/fttrigon.c | 277 ++ src/freetype/fttrigon.h | 125 + src/freetype/fttypes.h | 602 ++++ src/freetype/ftutil.c | 352 ++ src/freetype/ftvalid.h | 154 + src/freetype/internal.h | 72 + src/freetype/psarrst.c | 241 ++ src/freetype/psarrst.h | 100 + src/freetype/psaux.h | 1307 +++++++ src/freetype/psauxerr.h | 42 + src/freetype/psauxmod.c | 170 + src/freetype/psauxmod.h | 47 + src/freetype/psblues.c | 582 +++ src/freetype/psblues.h | 185 + src/freetype/psconv.c | 611 ++++ src/freetype/psconv.h | 71 + src/freetype/pserror.c | 52 + src/freetype/pserror.h | 119 + src/freetype/psfixed.h | 95 + src/freetype/psfont.c | 567 +++ src/freetype/psfont.h | 134 + src/freetype/psft.c | 843 +++++ src/freetype/psft.h | 160 + src/freetype/psglue.h | 144 + src/freetype/pshalgo.c | 2193 ++++++++++++ src/freetype/pshalgo.h | 241 ++ src/freetype/pshglob.c | 795 +++++ src/freetype/pshglob.h | 196 ++ src/freetype/pshints.c | 1939 ++++++++++ src/freetype/pshints.h | 700 ++++ src/freetype/pshints_.h | 288 ++ src/freetype/pshmod.c | 120 + src/freetype/pshmod.h | 39 + src/freetype/pshnterr.h | 41 + src/freetype/pshrec.c | 1220 +++++++ src/freetype/pshrec.h | 172 + src/freetype/psintrp.c | 3040 ++++++++++++++++ src/freetype/psintrp.h | 83 + src/freetype/psmodule.h | 38 + src/freetype/psnamerr.h | 42 + src/freetype/psobjs.c | 2533 ++++++++++++++ src/freetype/psobjs.h | 313 ++ src/freetype/psread.c | 112 + src/freetype/psread.h | 68 + src/freetype/psstack.c | 328 ++ src/freetype/psstack.h | 121 + src/freetype/pstables.h | 4238 ++++++++++++++++++++++ src/freetype/pstypes.h | 78 + src/freetype/rasterrs.h | 42 + src/freetype/sfdriver.c | 226 ++ src/freetype/sfdriver.h | 38 + src/freetype/sferrors.h | 41 + src/freetype/sfnt.h | 669 ++++ src/freetype/sfobjs.c | 1763 ++++++++++ src/freetype/sfobjs.h | 59 + src/freetype/svcfftl.h | 90 + src/freetype/svfntfmt.h | 55 + src/freetype/svgldict.h | 66 + src/freetype/svmetric.h | 125 + src/freetype/svpfr.h | 66 + src/freetype/svpscmap.h | 146 + src/freetype/svttcmap.h | 86 + src/freetype/svttglyf.h | 56 + src/freetype/t1cmap.c | 347 ++ src/freetype/t1cmap.h | 105 + src/freetype/t1decode.c | 479 +++ src/freetype/t1decode.h | 74 + src/freetype/t1driver.c | 134 + src/freetype/t1driver.h | 38 + src/freetype/t1errors.h | 41 + src/freetype/t1gload.c | 590 ++++ src/freetype/t1gload.h | 53 + src/freetype/t1load.c | 1487 ++++++++ src/freetype/t1load.h | 72 + src/freetype/t1objs.c | 615 ++++ src/freetype/t1objs.h | 160 + src/freetype/t1parse.c | 525 +++ src/freetype/t1parse.h | 129 + src/freetype/t1tables.h | 454 +++ src/freetype/t1tokens.h | 120 + src/freetype/t1types.h | 208 ++ src/freetype/ttcmap.c | 3282 +++++++++++++++++ src/freetype/ttcmap.h | 112 + src/freetype/ttcmapc.h | 56 + src/freetype/ttdriver.c | 472 +++ src/freetype/ttdriver.h | 38 + src/freetype/tterrors.h | 42 + src/freetype/ttgload.c | 2558 ++++++++++++++ src/freetype/ttgload.h | 62 + src/freetype/ttinterp.c | 8546 +++++++++++++++++++++++++++++++++++++++++++++ src/freetype/ttinterp.h | 539 +++ src/freetype/ttload.c | 1291 +++++++ src/freetype/ttload.h | 102 + src/freetype/ttmtx.c | 289 ++ src/freetype/ttmtx.h | 55 + src/freetype/ttnameid.h | 714 ++++ src/freetype/ttobjs.c | 1403 ++++++++ src/freetype/ttobjs.h | 425 +++ src/freetype/ttpload.c | 633 ++++ src/freetype/ttpload.h | 75 + src/freetype/ttpost.c | 575 +++ src/freetype/ttpost.h | 46 + src/freetype/ttsbit.c | 1680 +++++++++ src/freetype/ttsbit.h | 63 + src/freetype/tttables.h | 578 +++ src/freetype/tttags.h | 115 + src/freetype/tttypes.h | 1479 ++++++++ src/interop_ios.m | 1821 ++++++++++ src/interop_web.js | 1448 ++++++++ src/main.c | 177 + 431 files changed, 214316 insertions(+) create mode 100644 src/Animations.c create mode 100644 src/Animations.h create mode 100644 src/Audio.c create mode 100644 src/Audio.h create mode 100644 src/AudioBackend.c create mode 100644 src/AxisLinesRenderer.c create mode 100644 src/AxisLinesRenderer.h create mode 100644 src/Bitmap.c create mode 100644 src/Bitmap.h create mode 100644 src/Block.c create mode 100644 src/Block.h create mode 100644 src/BlockID.h create mode 100644 src/BlockPhysics.c create mode 100644 src/BlockPhysics.h create mode 100644 src/Builder.c create mode 100644 src/Builder.h create mode 100644 src/Camera.c create mode 100644 src/Camera.h create mode 100644 src/Chat.c create mode 100644 src/Chat.h create mode 100644 src/Commands.c create mode 100644 src/Commands.h create mode 100644 src/Constants.h create mode 100644 src/Core.h create mode 100644 src/Deflate.c create mode 100644 src/Deflate.h create mode 100644 src/Drawer.c create mode 100644 src/Drawer.h create mode 100644 src/Drawer2D.c create mode 100644 src/Drawer2D.h create mode 100644 src/Entity.c create mode 100644 src/Entity.h create mode 100644 src/EntityComponents.c create mode 100644 src/EntityComponents.h create mode 100644 src/EntityRenderers.c create mode 100644 src/EntityRenderers.h create mode 100644 src/EnvRenderer.c create mode 100644 src/EnvRenderer.h create mode 100644 src/Errors.h create mode 100644 src/Event.c create mode 100644 src/Event.h create mode 100644 src/ExtMath.c create mode 100644 src/ExtMath.h create mode 100644 src/FancyLighting.c create mode 100644 src/Formats.c create mode 100644 src/Formats.h create mode 100644 src/Funcs.h create mode 100644 src/Game.c create mode 100644 src/Game.h create mode 100644 src/GameVersion.c create mode 100644 src/Generator.c create mode 100644 src/Generator.h create mode 100644 src/Graphics.h create mode 100644 src/Graphics_3DS.c create mode 100644 src/Graphics_D3D11.c create mode 100644 src/Graphics_D3D9.c create mode 100644 src/Graphics_Dreamcast.c create mode 100644 src/Graphics_GCWii.c create mode 100644 src/Graphics_GL1.c create mode 100644 src/Graphics_GL2.c create mode 100644 src/Graphics_N64.c create mode 100644 src/Graphics_NDS.c create mode 100644 src/Graphics_PS1.c create mode 100644 src/Graphics_PS2.c create mode 100644 src/Graphics_PS3.c create mode 100644 src/Graphics_PSP.c create mode 100644 src/Graphics_PSVita.c create mode 100644 src/Graphics_Saturn.c create mode 100644 src/Graphics_SoftGPU.c create mode 100644 src/Graphics_WiiU.c create mode 100644 src/Graphics_Xbox.c create mode 100644 src/Graphics_Xbox360.c create mode 100644 src/Gui.c create mode 100644 src/Gui.h create mode 100644 src/HeldBlockRenderer.c create mode 100644 src/HeldBlockRenderer.h create mode 100644 src/Http.h create mode 100644 src/Http_Web.c create mode 100644 src/Http_Worker.c create mode 100644 src/Input.c create mode 100644 src/Input.h create mode 100644 src/Inventory.c create mode 100644 src/Inventory.h create mode 100644 src/IsometricDrawer.c create mode 100644 src/IsometricDrawer.h create mode 100644 src/LBackend.c create mode 100644 src/LBackend.h create mode 100644 src/LBackend_Android.c create mode 100644 src/LScreens.c create mode 100644 src/LScreens.h create mode 100644 src/LWeb.c create mode 100644 src/LWeb.h create mode 100644 src/LWidgets.c create mode 100644 src/LWidgets.h create mode 100644 src/Launcher.c create mode 100644 src/Launcher.h create mode 100644 src/Lighting.c create mode 100644 src/Lighting.h create mode 100644 src/Logger.c create mode 100644 src/Logger.h create mode 100644 src/MapRenderer.c create mode 100644 src/MapRenderer.h create mode 100644 src/Menus.c create mode 100644 src/Menus.h create mode 100644 src/Model.c create mode 100644 src/Model.h create mode 100644 src/Options.c create mode 100644 src/Options.h create mode 100644 src/PackedCol.c create mode 100644 src/PackedCol.h create mode 100644 src/Particle.c create mode 100644 src/Particle.h create mode 100644 src/Physics.c create mode 100644 src/Physics.h create mode 100644 src/Picking.c create mode 100644 src/Picking.h create mode 100644 src/Platform.h create mode 100644 src/Platform_3DS.c create mode 100644 src/Platform_Android.c create mode 100644 src/Platform_BeOS.cpp create mode 100644 src/Platform_Dreamcast.c create mode 100644 src/Platform_GCWii.c create mode 100644 src/Platform_MacClassic.c create mode 100644 src/Platform_N64.c create mode 100644 src/Platform_NDS.c create mode 100644 src/Platform_PS1.c create mode 100644 src/Platform_PS2.c create mode 100644 src/Platform_PS3.c create mode 100644 src/Platform_PSP.c create mode 100644 src/Platform_PSVita.c create mode 100644 src/Platform_Posix.c create mode 100644 src/Platform_Saturn.c create mode 100644 src/Platform_Switch.c create mode 100644 src/Platform_Web.c create mode 100644 src/Platform_WiiU.c create mode 100644 src/Platform_Windows.c create mode 100644 src/Platform_Xbox.c create mode 100644 src/Platform_Xbox360.c create mode 100644 src/Protocol.c create mode 100644 src/Protocol.h create mode 100644 src/Queue.c create mode 100644 src/Queue.h create mode 100644 src/Resources.c create mode 100644 src/Resources.h create mode 100644 src/SSL.c create mode 100644 src/SSL.h create mode 100644 src/Screens.c create mode 100644 src/Screens.h create mode 100644 src/SelOutlineRenderer.c create mode 100644 src/SelOutlineRenderer.h create mode 100644 src/SelectionBox.c create mode 100644 src/SelectionBox.h create mode 100644 src/Server.c create mode 100644 src/Server.h create mode 100644 src/Stream.c create mode 100644 src/Stream.h create mode 100644 src/String.c create mode 100644 src/String.h create mode 100644 src/SystemFonts.c create mode 100644 src/SystemFonts.h create mode 100644 src/TexturePack.c create mode 100644 src/TexturePack.h create mode 100644 src/TouchUI.c create mode 100644 src/Utils.c create mode 100644 src/Utils.h create mode 100644 src/Vectors.c create mode 100644 src/Vectors.h create mode 100644 src/VirtualKeyboard.h create mode 100644 src/Vorbis.c create mode 100644 src/Vorbis.h create mode 100644 src/Widgets.c create mode 100644 src/Widgets.h create mode 100644 src/Window.h create mode 100644 src/Window_3DS.c create mode 100644 src/Window_Android.c create mode 100644 src/Window_BeOS.cpp create mode 100644 src/Window_Dreamcast.c create mode 100644 src/Window_GCWii.c create mode 100644 src/Window_MacClassic.c create mode 100644 src/Window_N64.c create mode 100644 src/Window_NDS.c create mode 100644 src/Window_PS1.c create mode 100644 src/Window_PS2.c create mode 100644 src/Window_PS3.c create mode 100644 src/Window_PSP.c create mode 100644 src/Window_PSVita.c create mode 100644 src/Window_SDL.c create mode 100644 src/Window_SDL3.c create mode 100644 src/Window_Saturn.c create mode 100644 src/Window_Switch.c create mode 100644 src/Window_Terminal.c create mode 100644 src/Window_Web.c create mode 100644 src/Window_WiiU.cpp create mode 100644 src/Window_Win.c create mode 100644 src/Window_X11.c create mode 100644 src/Window_Xbox.c create mode 100644 src/Window_Xbox360.c create mode 100644 src/Window_cocoa.m create mode 100644 src/World.c create mode 100644 src/World.h create mode 100644 src/_GLShared.h create mode 100644 src/_GraphicsBase.h create mode 100644 src/_HttpBase.h create mode 100644 src/_PlatformBase.h create mode 100644 src/_PlatformConsole.h create mode 100644 src/_WindowBase.h create mode 100644 src/_autofit.c create mode 100644 src/_cff.c create mode 100644 src/_ftbase.c create mode 100644 src/_ftbitmap.c create mode 100644 src/_ftglyph.c create mode 100644 src/_ftinit.c create mode 100644 src/_ftsynth.c create mode 100644 src/_psaux.c create mode 100644 src/_pshinter.c create mode 100644 src/_psmodule.c create mode 100644 src/_sfnt.c create mode 100644 src/_smooth.c create mode 100644 src/_truetype.c create mode 100644 src/_type1.c create mode 100644 src/freetype/README (2) create mode 100644 src/freetype/afangles.c create mode 100644 src/freetype/afangles.h create mode 100644 src/freetype/afblue.c create mode 100644 src/freetype/afblue.h create mode 100644 src/freetype/afcover.h create mode 100644 src/freetype/afdummy.c create mode 100644 src/freetype/afdummy.h create mode 100644 src/freetype/aferrors.h create mode 100644 src/freetype/afglobal.c create mode 100644 src/freetype/afglobal.h create mode 100644 src/freetype/afhints.c create mode 100644 src/freetype/afhints.h create mode 100644 src/freetype/aflatin.c create mode 100644 src/freetype/aflatin.h create mode 100644 src/freetype/afloader.c create mode 100644 src/freetype/afloader.h create mode 100644 src/freetype/afmodule.c create mode 100644 src/freetype/afmodule.h create mode 100644 src/freetype/afranges.c create mode 100644 src/freetype/afranges.h create mode 100644 src/freetype/afscript.h create mode 100644 src/freetype/afshaper.c create mode 100644 src/freetype/afshaper.h create mode 100644 src/freetype/afstyles.h create mode 100644 src/freetype/aftypes.h create mode 100644 src/freetype/afwarp.c create mode 100644 src/freetype/afwarp.h create mode 100644 src/freetype/afwrtsys.h create mode 100644 src/freetype/autohint.h create mode 100644 src/freetype/cffcmap.c create mode 100644 src/freetype/cffcmap.h create mode 100644 src/freetype/cffdecode.c create mode 100644 src/freetype/cffdecode.h create mode 100644 src/freetype/cffdrivr.c create mode 100644 src/freetype/cffdrivr.h create mode 100644 src/freetype/cfferrs.h create mode 100644 src/freetype/cffgload.c create mode 100644 src/freetype/cffgload.h create mode 100644 src/freetype/cffload.c create mode 100644 src/freetype/cffload.h create mode 100644 src/freetype/cffobjs.c create mode 100644 src/freetype/cffobjs.h create mode 100644 src/freetype/cffotypes.h create mode 100644 src/freetype/cffparse.c create mode 100644 src/freetype/cffparse.h create mode 100644 src/freetype/cfftoken.h create mode 100644 src/freetype/cfftypes.h create mode 100644 src/freetype/fnterrs.h create mode 100644 src/freetype/freetype.h create mode 100644 src/freetype/ft2build.h create mode 100644 src/freetype/ftadvanc.c create mode 100644 src/freetype/ftadvanc.h create mode 100644 src/freetype/ftapi.c create mode 100644 src/freetype/ftbase.h create mode 100644 src/freetype/ftbitmap.h create mode 100644 src/freetype/ftcalc.c create mode 100644 src/freetype/ftcalc.h create mode 100644 src/freetype/ftconfig.h create mode 100644 src/freetype/ftdebug.h create mode 100644 src/freetype/ftdriver.h create mode 100644 src/freetype/ftdrv.h create mode 100644 src/freetype/fterrdef.h create mode 100644 src/freetype/fterrors.h create mode 100644 src/freetype/ftfntfmt.c create mode 100644 src/freetype/ftfntfmt.h create mode 100644 src/freetype/ftgloadr.c create mode 100644 src/freetype/ftgloadr.h create mode 100644 src/freetype/ftglyph.h create mode 100644 src/freetype/ftgrays.c create mode 100644 src/freetype/ftgrays.h create mode 100644 src/freetype/fthash.c create mode 100644 src/freetype/fthash.h create mode 100644 src/freetype/ftheader.h create mode 100644 src/freetype/ftimage.h create mode 100644 src/freetype/ftlist.h create mode 100644 src/freetype/ftmac.c create mode 100644 src/freetype/ftmac.h create mode 100644 src/freetype/ftmemory.h create mode 100644 src/freetype/ftmisc.h create mode 100644 src/freetype/ftmodapi.h create mode 100644 src/freetype/ftmoderr.h create mode 100644 src/freetype/ftmodule.h create mode 100644 src/freetype/ftobjs.c create mode 100644 src/freetype/ftobjs.h create mode 100644 src/freetype/ftoption.h create mode 100644 src/freetype/ftoutln.c create mode 100644 src/freetype/ftoutln.h create mode 100644 src/freetype/ftparams.h create mode 100644 src/freetype/ftrender.h create mode 100644 src/freetype/ftserv.h create mode 100644 src/freetype/ftsizes.h create mode 100644 src/freetype/ftsmerrs.h create mode 100644 src/freetype/ftsmooth.c create mode 100644 src/freetype/ftsmooth.h create mode 100644 src/freetype/ftstdlib.h create mode 100644 src/freetype/ftstream.c create mode 100644 src/freetype/ftstream.h create mode 100644 src/freetype/ftsynth.h create mode 100644 src/freetype/ftsystem.h create mode 100644 src/freetype/fttrace.h create mode 100644 src/freetype/fttrigon.c create mode 100644 src/freetype/fttrigon.h create mode 100644 src/freetype/fttypes.h create mode 100644 src/freetype/ftutil.c create mode 100644 src/freetype/ftvalid.h create mode 100644 src/freetype/internal.h create mode 100644 src/freetype/psarrst.c create mode 100644 src/freetype/psarrst.h create mode 100644 src/freetype/psaux.h create mode 100644 src/freetype/psauxerr.h create mode 100644 src/freetype/psauxmod.c create mode 100644 src/freetype/psauxmod.h create mode 100644 src/freetype/psblues.c create mode 100644 src/freetype/psblues.h create mode 100644 src/freetype/psconv.c create mode 100644 src/freetype/psconv.h create mode 100644 src/freetype/pserror.c create mode 100644 src/freetype/pserror.h create mode 100644 src/freetype/psfixed.h create mode 100644 src/freetype/psfont.c create mode 100644 src/freetype/psfont.h create mode 100644 src/freetype/psft.c create mode 100644 src/freetype/psft.h create mode 100644 src/freetype/psglue.h create mode 100644 src/freetype/pshalgo.c create mode 100644 src/freetype/pshalgo.h create mode 100644 src/freetype/pshglob.c create mode 100644 src/freetype/pshglob.h create mode 100644 src/freetype/pshints.c create mode 100644 src/freetype/pshints.h create mode 100644 src/freetype/pshints_.h create mode 100644 src/freetype/pshmod.c create mode 100644 src/freetype/pshmod.h create mode 100644 src/freetype/pshnterr.h create mode 100644 src/freetype/pshrec.c create mode 100644 src/freetype/pshrec.h create mode 100644 src/freetype/psintrp.c create mode 100644 src/freetype/psintrp.h create mode 100644 src/freetype/psmodule.h create mode 100644 src/freetype/psnamerr.h create mode 100644 src/freetype/psobjs.c create mode 100644 src/freetype/psobjs.h create mode 100644 src/freetype/psread.c create mode 100644 src/freetype/psread.h create mode 100644 src/freetype/psstack.c create mode 100644 src/freetype/psstack.h create mode 100644 src/freetype/pstables.h create mode 100644 src/freetype/pstypes.h create mode 100644 src/freetype/rasterrs.h create mode 100644 src/freetype/sfdriver.c create mode 100644 src/freetype/sfdriver.h create mode 100644 src/freetype/sferrors.h create mode 100644 src/freetype/sfnt.h create mode 100644 src/freetype/sfobjs.c create mode 100644 src/freetype/sfobjs.h create mode 100644 src/freetype/svcfftl.h create mode 100644 src/freetype/svfntfmt.h create mode 100644 src/freetype/svgldict.h create mode 100644 src/freetype/svmetric.h create mode 100644 src/freetype/svpfr.h create mode 100644 src/freetype/svpscmap.h create mode 100644 src/freetype/svttcmap.h create mode 100644 src/freetype/svttglyf.h create mode 100644 src/freetype/t1cmap.c create mode 100644 src/freetype/t1cmap.h create mode 100644 src/freetype/t1decode.c create mode 100644 src/freetype/t1decode.h create mode 100644 src/freetype/t1driver.c create mode 100644 src/freetype/t1driver.h create mode 100644 src/freetype/t1errors.h create mode 100644 src/freetype/t1gload.c create mode 100644 src/freetype/t1gload.h create mode 100644 src/freetype/t1load.c create mode 100644 src/freetype/t1load.h create mode 100644 src/freetype/t1objs.c create mode 100644 src/freetype/t1objs.h create mode 100644 src/freetype/t1parse.c create mode 100644 src/freetype/t1parse.h create mode 100644 src/freetype/t1tables.h create mode 100644 src/freetype/t1tokens.h create mode 100644 src/freetype/t1types.h create mode 100644 src/freetype/ttcmap.c create mode 100644 src/freetype/ttcmap.h create mode 100644 src/freetype/ttcmapc.h create mode 100644 src/freetype/ttdriver.c create mode 100644 src/freetype/ttdriver.h create mode 100644 src/freetype/tterrors.h create mode 100644 src/freetype/ttgload.c create mode 100644 src/freetype/ttgload.h create mode 100644 src/freetype/ttinterp.c create mode 100644 src/freetype/ttinterp.h create mode 100644 src/freetype/ttload.c create mode 100644 src/freetype/ttload.h create mode 100644 src/freetype/ttmtx.c create mode 100644 src/freetype/ttmtx.h create mode 100644 src/freetype/ttnameid.h create mode 100644 src/freetype/ttobjs.c create mode 100644 src/freetype/ttobjs.h create mode 100644 src/freetype/ttpload.c create mode 100644 src/freetype/ttpload.h create mode 100644 src/freetype/ttpost.c create mode 100644 src/freetype/ttpost.h create mode 100644 src/freetype/ttsbit.c create mode 100644 src/freetype/ttsbit.h create mode 100644 src/freetype/tttables.h create mode 100644 src/freetype/tttags.h create mode 100644 src/freetype/tttypes.h create mode 100644 src/interop_ios.m create mode 100644 src/interop_web.js create mode 100644 src/main.c (limited to 'src') diff --git a/src/Animations.c b/src/Animations.c new file mode 100644 index 0000000..5242c26 --- /dev/null +++ b/src/Animations.c @@ -0,0 +1,409 @@ +#include "TexturePack.h" +#include "String.h" +#include "Constants.h" +#include "Stream.h" +#include "Graphics.h" +#include "Event.h" +#include "Game.h" +#include "Funcs.h" +#include "Errors.h" +#include "Chat.h" +#include "ExtMath.h" +#include "Options.h" +#include "Logger.h" + +#ifdef CC_BUILD_ANIMATIONS +static void Animations_Update(int loc, struct Bitmap* bmp, int stride); + +#ifdef CC_BUILD_LOWMEM + #define LIQUID_ANIM_MAX 16 +#else + #define LIQUID_ANIM_MAX 64 +#endif + +#define WATER_TEX_LOC 14 +#define LAVA_TEX_LOC 30 + +#ifndef CC_BUILD_WEB +/* Based off the incredible work from https://dl.dropboxusercontent.com/u/12694594/lava.txt + mirrored at https://github.com/UnknownShadow200/ClassiCube/wiki/Minecraft-Classic-lava-animation-algorithm + Water animation originally written by cybertoon, big thanks! +*/ +/*########################################################################################################################* +*-----------------------------------------------------Lava animation------------------------------------------------------* +*#########################################################################################################################*/ +static float L_soupHeat[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; +static float L_potHeat[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; +static float L_flameHeat[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; +static RNGState L_rnd; +static cc_bool L_rndInited; + +static void LavaAnimation_Tick(void) { + BitmapCol pixels[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; + BitmapCol* ptr = pixels; + float soupHeat, potHeat, color; + int size, mask, shift; + int x, y, i = 0; + struct Bitmap bmp; + + size = min(Atlas2D.TileSize, LIQUID_ANIM_MAX); + mask = size - 1; + shift = Math_ilog2(size); + + if (!L_rndInited) { + Random_SeedFromCurrentTime(&L_rnd); + L_rndInited = true; + } + + for (y = 0; y < size; y++) { + for (x = 0; x < size; x++) { + /* Calculate the color at this coordinate in the heatmap */ + + /* Lookup table for (int)(1.2 * sin([ANGLE] * 22.5 * MATH_DEG2RAD)); */ + /* [ANGLE] is integer x/y, so repeats every 16 intervals */ + static cc_int8 sin_adj_table[16] = { 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1, 0, 0 }; + int xx = x + sin_adj_table[y & 0xF], yy = y + sin_adj_table[x & 0xF]; + + soupHeat = + L_soupHeat[((yy - 1) & mask) << shift | ((xx - 1) & mask)] + + L_soupHeat[((yy - 1) & mask) << shift | (xx & mask)] + + L_soupHeat[((yy - 1) & mask) << shift | ((xx + 1) & mask)] + + + L_soupHeat[(yy & mask) << shift | ((xx - 1) & mask)] + + L_soupHeat[(yy & mask) << shift | (xx & mask)] + + L_soupHeat[(yy & mask) << shift | ((xx + 1) & mask)] + + + L_soupHeat[((yy + 1) & mask) << shift | ((xx - 1) & mask)] + + L_soupHeat[((yy + 1) & mask) << shift | (xx & mask)] + + L_soupHeat[((yy + 1) & mask) << shift | ((xx + 1) & mask)]; + + potHeat = + L_potHeat[i] + /* x , y */ + L_potHeat[y << shift | ((x + 1) & mask)] + /* x + 1, y */ + L_potHeat[((y + 1) & mask) << shift | x] + /* x , y + 1 */ + L_potHeat[((y + 1) & mask) << shift | ((x + 1) & mask)];/* x + 1, y + 1 */ + + L_soupHeat[i] = soupHeat * 0.1f + potHeat * 0.2f; + + L_potHeat[i] += L_flameHeat[i]; + if (L_potHeat[i] < 0.0f) L_potHeat[i] = 0.0f; + + L_flameHeat[i] -= 0.06f * 0.01f; + if (Random_Float(&L_rnd) <= 0.005f) L_flameHeat[i] = 1.5f * 0.01f; + + /* Output the pixel */ + color = 2.0f * L_soupHeat[i]; + Math_Clamp(color, 0.0f, 1.0f); + + *ptr = BitmapCol_Make( + color * 100.0f + 155.0f, + color * color * 255.0f, + color * color * color * color * 128.0f, + 255); + + ptr++; i++; + } + } + + Bitmap_Init(bmp, size, size, pixels); + Animations_Update(LAVA_TEX_LOC, &bmp, size); +} + + +/*########################################################################################################################* +*----------------------------------------------------Water animation------------------------------------------------------* +*#########################################################################################################################*/ +static float W_soupHeat[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; +static float W_potHeat[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; +static float W_flameHeat[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; +static RNGState W_rnd; +static cc_bool W_rndInited; + +static void WaterAnimation_Tick(void) { + BitmapCol pixels[LIQUID_ANIM_MAX * LIQUID_ANIM_MAX]; + BitmapCol* ptr = pixels; + float soupHeat, color; + int size, mask, shift; + int x, y, i = 0; + struct Bitmap bmp; + + size = min(Atlas2D.TileSize, LIQUID_ANIM_MAX); + mask = size - 1; + shift = Math_ilog2(size); + + if (!W_rndInited) { + Random_SeedFromCurrentTime(&W_rnd); + W_rndInited = true; + } + + for (y = 0; y < size; y++) { + for (x = 0; x < size; x++) { + /* Calculate the color at this coordinate in the heatmap */ + soupHeat = + W_soupHeat[y << shift | ((x - 1) & mask)] + + W_soupHeat[y << shift | x ] + + W_soupHeat[y << shift | ((x + 1) & mask)]; + + W_soupHeat[i] = soupHeat / 3.3f + W_potHeat[i] * 0.8f; + + W_potHeat[i] += W_flameHeat[i]; + if (W_potHeat[i] < 0.0f) W_potHeat[i] = 0.0f; + + W_flameHeat[i] -= 0.1f * 0.05f; + if (Random_Float(&W_rnd) <= 0.05f) W_flameHeat[i] = 0.5f * 0.05f; + + /* Output the pixel */ + color = W_soupHeat[i]; + Math_Clamp(color, 0.0f, 1.0f); + color = color * color; + + *ptr = BitmapCol_Make( + 32.0f + color * 32.0f, + 50.0f + color * 64.0f, + 255, + 146.0f + color * 50.0f); + + ptr++; i++; + } + } + + Bitmap_Init(bmp, size, size, pixels); + Animations_Update(WATER_TEX_LOC, &bmp, size); +} +#endif + + +/*########################################################################################################################* +*-------------------------------------------------------Animations--------------------------------------------------------* +*#########################################################################################################################*/ +struct AnimationData { + TextureLoc texLoc; /* Tile (not pixel) coordinates in terrain.png */ + cc_uint16 frameX, frameY; /* Top left pixel coordinates of start frame in animations.png */ + cc_uint16 frameSize; /* Size of each frame in pixel coordinates */ + cc_uint16 state; /* Current animation frame index */ + cc_uint16 statesCount; /* Total number of animation frames */ + cc_uint16 delay; /* Delay in ticks until next frame is drawn */ + cc_uint16 frameDelay; /* Delay between each frame */ +}; + +static struct Bitmap anims_bmp; +static struct AnimationData anims_list[ATLAS1D_MAX_ATLASES]; +static int anims_count; +static cc_bool anims_validated, useLavaAnim, useWaterAnim, alwaysLavaAnim, alwaysWaterAnim; +#define ANIM_MIN_ARGS 7 + +static void Animations_ReadDescription(struct Stream* stream, const cc_string* path) { + cc_string line; char lineBuffer[STRING_SIZE * 2]; + cc_string parts[ANIM_MIN_ARGS]; + int count; + struct AnimationData data = { 0 }; + cc_uint8 tileX, tileY; + + cc_uint8 buffer[2048]; + struct Stream buffered; + cc_result res; + + String_InitArray(line, lineBuffer); + /* ReadLine reads single byte at a time */ + Stream_ReadonlyBuffered(&buffered, stream, buffer, sizeof(buffer)); + + for (;;) { + res = Stream_ReadLine(&buffered, &line); + if (res == ERR_END_OF_STREAM) break; + if (res) { Logger_SysWarn2(res, "reading from", path); break; } + + if (!line.length || line.buffer[0] == '#') continue; + count = String_UNSAFE_Split(&line, ' ', parts, ANIM_MIN_ARGS); + if (count < ANIM_MIN_ARGS) { + Chat_Add1("&cNot enough arguments for anim: %s", &line); continue; + } + + if (!Convert_ParseUInt8(&parts[0], &tileX) || tileX >= ATLAS2D_TILES_PER_ROW) { + Chat_Add1("&cInvalid anim tile X coord: %s", &parts[0]); continue; + } + if (!Convert_ParseUInt8(&parts[1], &tileY) || tileY >= ATLAS2D_MAX_ROWS_COUNT) { + Chat_Add1("&cInvalid anim tile Y coord: %s", &parts[1]); continue; + } + if (!Convert_ParseUInt16(&parts[2], &data.frameX)) { + Chat_Add1("&cInvalid anim frame X coord: %s", &parts[2]); continue; + } + if (!Convert_ParseUInt16(&parts[3], &data.frameY)) { + Chat_Add1("&cInvalid anim frame Y coord: %s", &parts[3]); continue; + } + if (!Convert_ParseUInt16(&parts[4], &data.frameSize) || !data.frameSize) { + Chat_Add1("&cInvalid anim frame size: %s", &parts[4]); continue; + } + if (!Convert_ParseUInt16(&parts[5], &data.statesCount)) { + Chat_Add1("&cInvalid anim states count: %s", &parts[5]); continue; + } + if (!Convert_ParseUInt16(&parts[6], &data.frameDelay)) { + Chat_Add1("&cInvalid anim frame delay: %s", &parts[6]); continue; + } + + if (anims_count == Array_Elems(anims_list)) { + Chat_AddRaw("&cCannot show over 512 animations"); return; + } + + data.texLoc = tileX + (tileY * ATLAS2D_TILES_PER_ROW); + anims_list[anims_count++] = data; + } +} + +static void Animations_Update(int texLoc, struct Bitmap* bmp, int stride) { + int dstX = Atlas1D_Index(texLoc); + int dstY = Atlas1D_RowId(texLoc) * Atlas2D.TileSize; + GfxResourceID tex; + + tex = Atlas1D.TexIds[dstX]; + if (tex) Gfx_UpdateTexture(tex, 0, dstY, bmp, stride, Gfx.Mipmaps); +} + +static void Animations_Apply(struct AnimationData* data) { + struct Bitmap frame; + int loc, size; + if (data->delay) { data->delay--; return; } + + data->state++; + data->state %= data->statesCount; + data->delay = data->frameDelay; + + loc = data->texLoc; +#ifndef CC_BUILD_WEB + if (loc == LAVA_TEX_LOC && useLavaAnim) return; + if (loc == WATER_TEX_LOC && useWaterAnim) return; +#endif + + size = data->frameSize; + Bitmap_Init(frame, size, size, NULL); + + frame.scan0 = anims_bmp.scan0 + + data->frameY * anims_bmp.width + + (data->frameX + data->state * size); + Animations_Update(loc, &frame, anims_bmp.width); +} + +static cc_bool Animations_IsDefaultZip(void) { + cc_string texPack; + cc_bool optExists; + if (TexturePack_Url.length) return false; + + optExists = Options_UNSAFE_Get(OPT_DEFAULT_TEX_PACK, &texPack); + return !optExists || String_CaselessEqualsConst(&texPack, "default.zip"); +} + +static void Animations_Clear(void) { + Mem_Free(anims_bmp.scan0); + anims_count = 0; + anims_bmp.scan0 = NULL; + anims_validated = false; +} + +static void Animations_Validate(void) { + struct AnimationData* data; + int maxX, maxY, tileX, tileY; + int i, j; + + anims_validated = true; + for (i = 0; i < anims_count; i++) { + data = &anims_list[i]; + + maxX = data->frameX + data->frameSize * data->statesCount; + maxY = data->frameY + data->frameSize; + tileX = Atlas2D_TileX(data->texLoc); + tileY = Atlas2D_TileY(data->texLoc); + + if (data->frameSize > Atlas2D.TileSize || tileY >= Atlas2D.RowsCount) { + Chat_Add2("&cAnimation frames for tile (%i, %i) are bigger than the size of a tile in terrain.png", &tileX, &tileY); + } else if (maxX > anims_bmp.width || maxY > anims_bmp.height) { + Chat_Add2("&cSome of the animation frames for tile (%i, %i) are at coordinates outside animations.png", &tileX, &tileY); + } else { + /* if user has water/lava animations in their default.zip, disable built-in */ + /* However, 'usewateranim' and 'uselavaanim' files should always disable use */ + /* of custom water/lava animations, even when they exist in animations.png */ + if (data->texLoc == LAVA_TEX_LOC && !alwaysLavaAnim) useLavaAnim = false; + if (data->texLoc == WATER_TEX_LOC && !alwaysWaterAnim) useWaterAnim = false; + continue; + } + + /* Remove this animation from the list */ + for (j = i; j < anims_count - 1; j++) { + anims_list[j] = anims_list[j + 1]; + } + i--; anims_count--; + } +} + +static void Animations_Tick(struct ScheduledTask* task) { + int i; +#ifndef CC_BUILD_WEB + if (useLavaAnim) LavaAnimation_Tick(); + if (useWaterAnim) WaterAnimation_Tick(); +#endif + + if (!anims_count) return; + if (!anims_bmp.scan0) { + Chat_AddRaw("&cCurrent texture pack specifies it uses animations,"); + Chat_AddRaw("&cbut is missing animations.png"); + anims_count = 0; return; + } + + /* deferred, because when reading animations.txt, might not have read animations.png yet */ + if (!anims_validated) Animations_Validate(); + for (i = 0; i < anims_count; i++) { + Animations_Apply(&anims_list[i]); + } +} + + +/*########################################################################################################################* +*--------------------------------------------------Animations component---------------------------------------------------* +*#########################################################################################################################*/ +static void AnimationsPngProcess(struct Stream* stream, const cc_string* name) { + cc_result res = Png_Decode(&anims_bmp, stream); + if (!res) return; + + Logger_SysWarn2(res, "decoding", name); + Mem_Free(anims_bmp.scan0); + anims_bmp.scan0 = NULL; +} +static struct TextureEntry animations_entry = { "animations.png", AnimationsPngProcess }; +static struct TextureEntry animations_txt = { "animations.txt", Animations_ReadDescription }; + +static void UseWaterProcess(struct Stream* stream, const cc_string* name) { + useWaterAnim = true; + alwaysWaterAnim = true; +} +static struct TextureEntry water_entry = { "usewateranim", UseWaterProcess }; + +static void UseLavaProcess(struct Stream* stream, const cc_string* name) { + useLavaAnim = true; + alwaysLavaAnim = true; +} +static struct TextureEntry lava_entry = { "uselavaanim", UseLavaProcess }; + + +static void OnPackChanged(void* obj) { + Animations_Clear(); + useLavaAnim = Animations_IsDefaultZip(); + useWaterAnim = useLavaAnim; + alwaysLavaAnim = false; + alwaysWaterAnim = false; +} +static void OnInit(void) { + TextureEntry_Register(&animations_entry); + TextureEntry_Register(&animations_txt); + TextureEntry_Register(&water_entry); + TextureEntry_Register(&lava_entry); + + ScheduledTask_Add(GAME_DEF_TICKS, Animations_Tick); + Event_Register_(&TextureEvents.PackChanged, NULL, OnPackChanged); +} +#else +static void Animations_Clear(void) { } +static void OnInit(void) { } +#endif + +struct IGameComponent Animations_Component = { + OnInit, /* Init */ + Animations_Clear /* Free */ +}; diff --git a/src/Animations.h b/src/Animations.h new file mode 100644 index 0000000..78f2d3d --- /dev/null +++ b/src/Animations.h @@ -0,0 +1,10 @@ +#ifndef CC_ANIMATIONS_H +#define CC_ANIMATIONS_H +/* +Contains everything relating to texture animations (including default water/lava ones) +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct IGameComponent; +extern struct IGameComponent Animations_Component; +#endif diff --git a/src/Audio.c b/src/Audio.c new file mode 100644 index 0000000..74aa40b --- /dev/null +++ b/src/Audio.c @@ -0,0 +1,566 @@ +#include "Audio.h" +#include "String.h" +#include "Logger.h" +#include "Event.h" +#include "Block.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Game.h" +#include "Errors.h" +#include "Vorbis.h" +#include "Chat.h" +#include "Stream.h" +#include "Utils.h" +#include "Options.h" +#include "Deflate.h" +#ifdef CC_BUILD_ANDROID +/* TODO: Refactor maybe to not rely on checking WinInfo.Handle != NULL */ +#include "Window.h" +#endif + +int Audio_SoundsVolume, Audio_MusicVolume; +const cc_string Sounds_ZipPathMC = String_FromConst("audio/default.zip"); +const cc_string Sounds_ZipPathCC = String_FromConst("audio/classicube.zip"); +static const cc_string audio_dir = String_FromConst("audio"); + +struct Sound { + int channels, sampleRate; + struct AudioChunk chunk; +}; + + +/*########################################################################################################################* +*--------------------------------------------------------Sounds-----------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_NOSOUNDS +/* Can't use mojang's sound assets, so just stub everything out */ +static void Sounds_Init(void) { } +static void Sounds_Free(void) { } +static void Sounds_Stop(void) { } +static void Sounds_Start(void) { + Chat_AddRaw("&cSounds are not supported currently"); + Audio_SoundsVolume = 0; +} + +void Audio_PlayDigSound(cc_uint8 type) { } +void Audio_PlayStepSound(cc_uint8 type) { } +#else +#define AUDIO_MAX_SOUNDS 10 + +struct SoundGroup { + int count; + struct Sound sounds[AUDIO_MAX_SOUNDS]; +}; +struct Soundboard { struct SoundGroup groups[SOUND_COUNT]; }; + +static struct Soundboard digBoard, stepBoard; +static RNGState sounds_rnd; + +#define WAV_FourCC(a, b, c, d) (((cc_uint32)a << 24) | ((cc_uint32)b << 16) | ((cc_uint32)c << 8) | (cc_uint32)d) +#define WAV_FMT_SIZE 16 + +static cc_result Sound_ReadWaveData(struct Stream* stream, struct Sound* snd) { + cc_uint32 fourCC, size; + cc_uint8 tmp[WAV_FMT_SIZE]; + cc_result res; + int bitsPerSample; + + if ((res = Stream_Read(stream, tmp, 12))) return res; + fourCC = Stream_GetU32_BE(tmp + 0); + if (fourCC == WAV_FourCC('I','D','3', 2)) return AUDIO_ERR_MP3_SIG; /* ID3 v2.2 tags header */ + if (fourCC == WAV_FourCC('I','D','3', 3)) return AUDIO_ERR_MP3_SIG; /* ID3 v2.3 tags header */ + if (fourCC != WAV_FourCC('R','I','F','F')) return WAV_ERR_STREAM_HDR; + + /* tmp[4] (4) file size */ + fourCC = Stream_GetU32_BE(tmp + 8); + if (fourCC != WAV_FourCC('W','A','V','E')) return WAV_ERR_STREAM_TYPE; + + for (;;) { + if ((res = Stream_Read(stream, tmp, 8))) return res; + fourCC = Stream_GetU32_BE(tmp + 0); + size = Stream_GetU32_LE(tmp + 4); + + if (fourCC == WAV_FourCC('f','m','t',' ')) { + if ((res = Stream_Read(stream, tmp, sizeof(tmp)))) return res; + if (Stream_GetU16_LE(tmp + 0) != 1) return WAV_ERR_DATA_TYPE; + + snd->channels = Stream_GetU16_LE(tmp + 2); + snd->sampleRate = Stream_GetU32_LE(tmp + 4); + /* tmp[8] (6) alignment data and stuff */ + + bitsPerSample = Stream_GetU16_LE(tmp + 14); + if (bitsPerSample != 16) return WAV_ERR_SAMPLE_BITS; + size -= WAV_FMT_SIZE; + } else if (fourCC == WAV_FourCC('d','a','t','a')) { + if ((res = Audio_AllocChunks(size, &snd->chunk, 1))) return res; + res = Stream_Read(stream, snd->chunk.data, size); + + #ifdef CC_BUILD_BIGENDIAN + Utils_SwapEndian16((cc_int16*)snd->chunk.data, size / 2); + #endif + return res; + } + + /* Skip over unhandled data */ + if (size && (res = stream->Skip(stream, size))) return res; + } +} + +static struct SoundGroup* Soundboard_FindGroup(struct Soundboard* board, const cc_string* name) { + struct SoundGroup* groups = board->groups; + int i; + + for (i = 0; i < SOUND_COUNT; i++) + { + if (String_CaselessEqualsConst(name, Sound_Names[i])) return &groups[i]; + } + return NULL; +} + +static void Soundboard_Load(struct Soundboard* board, const cc_string* boardName, const cc_string* file, struct Stream* stream) { + struct SoundGroup* group; + struct Sound* snd; + cc_string name = *file; + cc_result res; + int dotIndex; + Utils_UNSAFE_TrimFirstDirectory(&name); + + /* dig_grass1.wav -> dig_grass1 */ + dotIndex = String_LastIndexOf(&name, '.'); + if (dotIndex >= 0) name.length = dotIndex; + if (!String_CaselessStarts(&name, boardName)) return; + + /* Convert dig_grass1 to grass */ + name = String_UNSAFE_SubstringAt(&name, boardName->length); + name = String_UNSAFE_Substring(&name, 0, name.length - 1); + + group = Soundboard_FindGroup(board, &name); + if (!group) { + Chat_Add1("&cUnknown sound group '%s'", &name); return; + } + if (group->count == Array_Elems(group->sounds)) { + Chat_AddRaw("&cCannot have more than 10 sounds in a group"); return; + } + + snd = &group->sounds[group->count]; + res = Sound_ReadWaveData(stream, snd); + + if (res) { + Logger_SysWarn2(res, "decoding", file); + Audio_FreeChunks(&snd->chunk, 1); + snd->chunk.data = NULL; + snd->chunk.size = 0; + } else { group->count++; } +} + +static const struct Sound* Soundboard_PickRandom(struct Soundboard* board, cc_uint8 type) { + struct SoundGroup* group; + int idx; + + if (type == SOUND_NONE || type >= SOUND_COUNT) return NULL; + if (type == SOUND_METAL) type = SOUND_STONE; + + group = &board->groups[type]; + if (!group->count) return NULL; + + idx = Random_Next(&sounds_rnd, group->count); + return &group->sounds[idx]; +} + + +CC_NOINLINE static void Sounds_Fail(cc_result res) { + Audio_Warn(res, "playing sounds"); + Chat_AddRaw("&cDisabling sounds"); + Audio_SetSounds(0); +} + +static void Sounds_Play(cc_uint8 type, struct Soundboard* board) { + const struct Sound* snd; + struct AudioData data; + cc_result res; + + if (type == SOUND_NONE || !Audio_SoundsVolume) return; + snd = Soundboard_PickRandom(board, type); + if (!snd) return; + + data.chunk = snd->chunk; + data.channels = snd->channels; + data.sampleRate = snd->sampleRate; + data.rate = 100; + data.volume = Audio_SoundsVolume; + + /* https://minecraft.wiki/w/Block_of_Gold#Sounds */ + /* https://minecraft.wiki/w/Grass#Sounds */ + if (board == &digBoard) { + if (type == SOUND_METAL) data.rate = 120; + else data.rate = 80; + } else { + data.volume /= 2; + if (type == SOUND_METAL) data.rate = 140; + } + + res = AudioPool_Play(&data); + if (res) Sounds_Fail(res); +} + +static void Audio_PlayBlockSound(void* obj, IVec3 coords, BlockID old, BlockID now) { + if (now == BLOCK_AIR) { + Audio_PlayDigSound(Blocks.DigSounds[old]); + } else if (!Game_ClassicMode) { + /* use StepSounds instead when placing, as don't want */ + /* to play glass break sound when placing glass */ + Audio_PlayDigSound(Blocks.StepSounds[now]); + } +} + +static cc_bool SelectZipEntry(const cc_string* path) { return true; } +static cc_result ProcessZipEntry(const cc_string* path, struct Stream* stream, struct ZipEntry* source) { + static const cc_string dig = String_FromConst("dig_"); + static const cc_string step = String_FromConst("step_"); + + Soundboard_Load(&digBoard, &dig, path, stream); + Soundboard_Load(&stepBoard, &step, path, stream); + return 0; +} + +static cc_result Sounds_ExtractZip(const cc_string* path) { + struct Stream stream; + cc_result res; + + res = Stream_OpenFile(&stream, path); + if (res) { Logger_SysWarn2(res, "opening", path); return res; } + + res = Zip_Extract(&stream, SelectZipEntry, ProcessZipEntry); + if (res) Logger_SysWarn2(res, "extracting", path); + + /* No point logging error for closing readonly file */ + (void)stream.Close(&stream); + return res; +} + +/* TODO this is a pretty terrible solution */ +#ifdef CC_BUILD_WEBAUDIO +static const struct SoundID { int group; const char* name; } sounds_list[] = +{ + { SOUND_CLOTH, "step_cloth1" }, { SOUND_CLOTH, "step_cloth2" }, { SOUND_CLOTH, "step_cloth3" }, { SOUND_CLOTH, "step_cloth4" }, + { SOUND_GRASS, "step_grass1" }, { SOUND_GRASS, "step_grass2" }, { SOUND_GRASS, "step_grass3" }, { SOUND_GRASS, "step_grass4" }, + { SOUND_GRAVEL, "step_gravel1" }, { SOUND_GRAVEL, "step_gravel2" }, { SOUND_GRAVEL, "step_gravel3" }, { SOUND_GRAVEL, "step_gravel4" }, + { SOUND_SAND, "step_sand1" }, { SOUND_SAND, "step_sand2" }, { SOUND_SAND, "step_sand3" }, { SOUND_SAND, "step_sand4" }, + { SOUND_SNOW, "step_snow1" }, { SOUND_SNOW, "step_snow2" }, { SOUND_SNOW, "step_snow3" }, { SOUND_SNOW, "step_snow4" }, + { SOUND_STONE, "step_stone1" }, { SOUND_STONE, "step_stone2" }, { SOUND_STONE, "step_stone3" }, { SOUND_STONE, "step_stone4" }, + { SOUND_WOOD, "step_wood1" }, { SOUND_WOOD, "step_wood2" }, { SOUND_WOOD, "step_wood3" }, { SOUND_WOOD, "step_wood4" }, + { SOUND_NONE, NULL }, + + { SOUND_CLOTH, "dig_cloth1" }, { SOUND_CLOTH, "dig_cloth2" }, { SOUND_CLOTH, "dig_cloth3" }, { SOUND_CLOTH, "dig_cloth4" }, + { SOUND_GRASS, "dig_grass1" }, { SOUND_GRASS, "dig_grass2" }, { SOUND_GRASS, "dig_grass3" }, { SOUND_GRASS, "dig_grass4" }, + { SOUND_GLASS, "dig_glass1" }, { SOUND_GLASS, "dig_glass2" }, { SOUND_GLASS, "dig_glass3" }, + { SOUND_GRAVEL, "dig_gravel1" }, { SOUND_GRAVEL, "dig_gravel2" }, { SOUND_GRAVEL, "dig_gravel3" }, { SOUND_GRAVEL, "dig_gravel4" }, + { SOUND_SAND, "dig_sand1" }, { SOUND_SAND, "dig_sand2" }, { SOUND_SAND, "dig_sand3" }, { SOUND_SAND, "dig_sand4" }, + { SOUND_SNOW, "dig_snow1" }, { SOUND_SNOW, "dig_snow2" }, { SOUND_SNOW, "dig_snow3" }, { SOUND_SNOW, "dig_snow4" }, + { SOUND_STONE, "dig_stone1" }, { SOUND_STONE, "dig_stone2" }, { SOUND_STONE, "dig_stone3" }, { SOUND_STONE, "dig_stone4" }, + { SOUND_WOOD, "dig_wood1" }, { SOUND_WOOD, "dig_wood2" }, { SOUND_WOOD, "dig_wood3" }, { SOUND_WOOD, "dig_wood4" }, +}; + +/* TODO this is a terrible solution */ +static void InitWebSounds(void) { + struct Soundboard* board = &stepBoard; + struct SoundGroup* group; + int i; + + for (i = 0; i < Array_Elems(sounds_list); i++) { + if (sounds_list[i].group == SOUND_NONE) { + board = &digBoard; + } else { + group = &board->groups[sounds_list[i].group]; + group->sounds[group->count++].chunk.data = sounds_list[i].name; + } + } +} +#endif + +static cc_bool sounds_loaded; +static void Sounds_Start(void) { + cc_result res; + if (!AudioBackend_Init()) { + AudioBackend_Free(); + Audio_SoundsVolume = 0; + return; + } + + if (sounds_loaded) return; + sounds_loaded = true; +#ifdef CC_BUILD_WEBAUDIO + InitWebSounds(); +#else + res = Sounds_ExtractZip(&Sounds_ZipPathMC); + if (res == ReturnCode_FileNotFound) + Sounds_ExtractZip(&Sounds_ZipPathCC); +#endif +} + +static void Sounds_Stop(void) { AudioPool_Close(); } + +static void Sounds_Init(void) { + int volume = Options_GetInt(OPT_SOUND_VOLUME, 0, 100, DEFAULT_SOUNDS_VOLUME); + Audio_SetSounds(volume); + Event_Register_(&UserEvents.BlockChanged, NULL, Audio_PlayBlockSound); +} +static void Sounds_Free(void) { Sounds_Stop(); } + +void Audio_PlayDigSound(cc_uint8 type) { Sounds_Play(type, &digBoard); } +void Audio_PlayStepSound(cc_uint8 type) { Sounds_Play(type, &stepBoard); } +#endif + + +/*########################################################################################################################* +*--------------------------------------------------------Music------------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_NOMUSIC +/* Can't use mojang's music assets, so just stub everything out */ +static void Music_Init(void) { } +static void Music_Free(void) { } +static void Music_Stop(void) { } +static void Music_Start(void) { + Chat_AddRaw("&cMusic is not supported currently"); + Audio_MusicVolume = 0; +} +#else +static void* music_thread; +static void* music_waitable; +static volatile cc_bool music_stopping, music_joining; +static int music_minDelay, music_maxDelay; + +static cc_result Music_Buffer(struct AudioChunk* chunk, int maxSamples, struct VorbisState* ctx) { + int samples = 0; + cc_int16* cur; + cc_result res = 0, res2; + cc_int16* data = chunk->data; + + while (samples < maxSamples) { + if ((res = Vorbis_DecodeFrame(ctx))) break; + + cur = &data[samples]; + samples += Vorbis_OutputFrame(ctx, cur); + } + + chunk->size = samples * 2; + res2 = Audio_QueueChunk(&music_ctx, chunk); + if (res2) { music_stopping = true; return res2; } + return res; +} + +static cc_result Music_PlayOgg(struct Stream* source) { + struct OggState ogg; + struct VorbisState vorbis; + int channels, sampleRate, volume; + + int chunkSize, samplesPerSecond; + struct AudioChunk chunks[AUDIO_MAX_BUFFERS] = { 0 }; + int inUse, i, cur; + cc_result res; + + Ogg_Init(&ogg, source); + Vorbis_Init(&vorbis); + vorbis.source = &ogg; + if ((res = Vorbis_DecodeHeaders(&vorbis))) goto cleanup; + + channels = vorbis.channels; + sampleRate = vorbis.sampleRate; + if ((res = Audio_SetFormat(&music_ctx, channels, sampleRate, 100))) goto cleanup; + + /* largest possible vorbis frame decodes to blocksize1 * channels samples, */ + /* so can end up decoding slightly over a second of audio */ + chunkSize = channels * (sampleRate + vorbis.blockSizes[1]); + samplesPerSecond = channels * sampleRate; + + if ((res = Audio_AllocChunks(chunkSize * 2, chunks, AUDIO_MAX_BUFFERS))) goto cleanup; + volume = Audio_MusicVolume; + Audio_SetVolume(&music_ctx, volume); + + /* fill up with some samples before playing */ + for (i = 0; i < AUDIO_MAX_BUFFERS && !res; i++) + { + res = Music_Buffer(&chunks[i], samplesPerSecond, &vorbis); + } + if (music_stopping) goto cleanup; + + res = Audio_Play(&music_ctx); + if (res) goto cleanup; + cur = 0; + + while (!music_stopping) { +#ifdef CC_BUILD_ANDROID + /* Don't play music while in the background on Android */ + /* TODO: Not use such a terrible approach */ + if (!Window_Main.Handle) { + Audio_Pause(&music_ctx); + while (!Window_Main.Handle && !music_stopping) { + Thread_Sleep(10); continue; + } + Audio_Play(&music_ctx); + } +#endif + if (volume != Audio_MusicVolume) { + volume = Audio_MusicVolume; + Audio_SetVolume(&music_ctx, volume); + } + + res = Audio_Poll(&music_ctx, &inUse); + if (res) { music_stopping = true; break; } + + if (inUse >= AUDIO_MAX_BUFFERS) { + Thread_Sleep(10); continue; + } + + res = Music_Buffer(&chunks[cur], samplesPerSecond, &vorbis); + cur = (cur + 1) % AUDIO_MAX_BUFFERS; + + /* need to specially handle last bit of audio */ + if (res) break; + } + + if (music_stopping) { + /* must close audio context, as otherwise some of the audio */ + /* context's internal audio buffers may have a reference */ + /* to the `data` buffer which will be freed after this */ + Audio_Close(&music_ctx); + } else { + /* Wait until the buffers finished playing */ + for (;;) { + if (Audio_Poll(&music_ctx, &inUse) || inUse == 0) break; + Thread_Sleep(10); + } + } + +cleanup: + Audio_FreeChunks(chunks, AUDIO_MAX_BUFFERS); + Vorbis_Free(&vorbis); + return res == ERR_END_OF_STREAM ? 0 : res; +} + +static void Music_AddFile(const cc_string* path, void* obj) { + struct StringsBuffer* files = (struct StringsBuffer*)obj; + static const cc_string ogg = String_FromConst(".ogg"); + + if (!String_CaselessEnds(path, &ogg)) return; + StringsBuffer_Add(files, path); +} + +static void Music_RunLoop(void) { + struct StringsBuffer files; + cc_string path; + RNGState rnd; + struct Stream stream; + int idx, delay; + cc_result res = 0; + + StringsBuffer_SetLengthBits(&files, STRINGSBUFFER_DEF_LEN_SHIFT); + StringsBuffer_Init(&files); + Directory_Enum(&audio_dir, &files, Music_AddFile); + + Random_SeedFromCurrentTime(&rnd); + res = Audio_Init(&music_ctx, AUDIO_MAX_BUFFERS); + if (res) music_stopping = true; + + while (!music_stopping && files.count) { + idx = Random_Next(&rnd, files.count); + path = StringsBuffer_UNSAFE_Get(&files, idx); + Platform_Log1("playing music file: %s", &path); + + res = Stream_OpenFile(&stream, &path); + if (res) { Logger_SysWarn2(res, "opening", &path); break; } + + res = Music_PlayOgg(&stream); + if (res) { Logger_SimpleWarn2(res, "playing", &path); } + + /* No point logging error for closing readonly file */ + (void)stream.Close(&stream); + + if (music_stopping) break; + delay = Random_Range(&rnd, music_minDelay, music_maxDelay); + Waitable_WaitFor(music_waitable, delay); + } + + if (res) { + Chat_AddRaw("&cDisabling music"); + Audio_MusicVolume = 0; + } + Audio_Close(&music_ctx); + StringsBuffer_Clear(&files); + + if (music_joining) return; + Thread_Detach(music_thread); + music_thread = NULL; +} + +static void Music_Start(void) { + if (music_thread) return; + if (!AudioBackend_Init()) { + AudioBackend_Free(); + Audio_MusicVolume = 0; + return; + } + + music_joining = false; + music_stopping = false; + + Thread_Run(&music_thread, Music_RunLoop, 256 * 1024, "Music"); +} + +static void Music_Stop(void) { + music_joining = true; + music_stopping = true; + Waitable_Signal(music_waitable); + + if (music_thread) Thread_Join(music_thread); + music_thread = NULL; +} + +static void Music_Init(void) { + int volume; + /* music is delayed between 2 - 7 minutes by default */ + music_minDelay = Options_GetInt(OPT_MIN_MUSIC_DELAY, 0, 3600, 120) * MILLIS_PER_SEC; + music_maxDelay = Options_GetInt(OPT_MAX_MUSIC_DELAY, 0, 3600, 420) * MILLIS_PER_SEC; + music_waitable = Waitable_Create("Music sleep"); + + volume = Options_GetInt(OPT_MUSIC_VOLUME, 0, 100, DEFAULT_MUSIC_VOLUME); + Audio_SetMusic(volume); +} + +static void Music_Free(void) { + Music_Stop(); + Waitable_Free(music_waitable); +} +#endif + + +/*########################################################################################################################* +*--------------------------------------------------------General----------------------------------------------------------* +*#########################################################################################################################*/ +void Audio_SetSounds(int volume) { + Audio_SoundsVolume = volume; + if (volume) Sounds_Start(); + else Sounds_Stop(); +} + +void Audio_SetMusic(int volume) { + Audio_MusicVolume = volume; + if (volume) Music_Start(); + else Music_Stop(); +} + +static void OnInit(void) { + Sounds_Init(); + Music_Init(); +} + +static void OnFree(void) { + Sounds_Free(); + Music_Free(); + AudioBackend_Free(); +} + +struct IGameComponent Audio_Component = { + OnInit, /* Init */ + OnFree /* Free */ +}; diff --git a/src/Audio.h b/src/Audio.h new file mode 100644 index 0000000..1ab2ae6 --- /dev/null +++ b/src/Audio.h @@ -0,0 +1,90 @@ +#ifndef CC_AUDIO_H +#define CC_AUDIO_H +#include "Core.h" +/* Manages playing sound and music. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent Audio_Component; +struct AudioContext; + +#ifdef CC_BUILD_WEBAUDIO +#define DEFAULT_SOUNDS_VOLUME 0 +#else +#define DEFAULT_SOUNDS_VOLUME 100 +#endif + +#ifdef CC_BUILD_NOMUSIC +#define DEFAULT_MUSIC_VOLUME 0 +#else +#define DEFAULT_MUSIC_VOLUME 100 +#endif + +union AudioChunkMeta { void* ptr; cc_uintptr val; }; +struct AudioChunk { + void* data; /* the raw 16 bit integer samples */ + cc_uint32 size; + union AudioChunkMeta meta; +}; + +struct AudioData { + struct AudioChunk chunk; + int channels; + int sampleRate; /* frequency / sample rate */ + int volume; /* volume data played at (100 = normal volume) */ + int rate; /* speed/pitch played at (100 = normal speed) */ +}; + +/* Volume sounds are played at, from 0-100. */ +/* NOTE: Use Audio_SetSounds, don't change this directly. */ +extern int Audio_SoundsVolume; +/* Volume music is played at, from 0-100. */ +/* NOTE: Use Audio_SetMusic, don't change this directly. */ +extern int Audio_MusicVolume; +extern const cc_string Sounds_ZipPathMC; +extern const cc_string Sounds_ZipPathCC; + +void Audio_SetMusic(int volume); +void Audio_SetSounds(int volume); +void Audio_PlayDigSound(cc_uint8 type); +void Audio_PlayStepSound(cc_uint8 type); +#define AUDIO_MAX_BUFFERS 4 + +cc_bool AudioBackend_Init(void); +void AudioBackend_Tick(void); +void AudioBackend_Free(void); + +/* Initialises an audio context. */ +cc_result Audio_Init(struct AudioContext* ctx, int buffers); +/* Stops any playing audio and then frees the audio context. */ +void Audio_Close(struct AudioContext* ctx); +/* Sets the format of the audio data to be played. */ +/* NOTE: Changing the format can be expensive, depending on the backend. */ +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate); +/* Sets the volume audio data is played at */ +void Audio_SetVolume(struct AudioContext* ctx, int volume); +/* Queues the given audio chunk for playing. */ +/* NOTE: You MUST ensure Audio_Poll indicates a buffer is free before calling this. */ +/* NOTE: Some backends directly read from the chunk data - therefore you MUST NOT modify it */ +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk); +/* Begins playing audio. Audio_QueueChunk must have been used before this. */ +cc_result Audio_Play(struct AudioContext* ctx); +/* Polls the audio context and then potentially unqueues buffer */ +/* Returns the number of buffers being played or queued */ +/* (e.g. if inUse is 0, no audio buffers are being played or queued) */ +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse); +cc_result Audio_Pause(struct AudioContext* ctx); /* Only implemented with OpenSL ES backend */ + +/* Outputs more detailed information about errors with audio. */ +cc_bool Audio_DescribeError(cc_result res, cc_string* dst); +/* Allocates a group of chunks of data to store audio samples */ +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks); +/* Frees a previously allocated group of chunks of data */ +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks); + +extern struct AudioContext music_ctx; +void Audio_Warn(cc_result res, const char* action); + +cc_result AudioPool_Play(struct AudioData* data); +void AudioPool_Close(void); +#endif diff --git a/src/AudioBackend.c b/src/AudioBackend.c new file mode 100644 index 0000000..62a647d --- /dev/null +++ b/src/AudioBackend.c @@ -0,0 +1,1717 @@ +#include "Audio.h" +#include "String.h" +#include "Logger.h" +#include "Funcs.h" +#include "Errors.h" +#include "Utils.h" +#include "Platform.h" + +void Audio_Warn(cc_result res, const char* action) { + Logger_Warn(res, action, Audio_DescribeError); +} + +/* Whether the given audio data can be played without recreating the underlying audio device */ +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data); + +/* Common/Base methods */ +static void AudioBase_Clear(struct AudioContext* ctx); +static cc_bool AudioBase_AdjustSound(struct AudioContext* ctx, int i, struct AudioChunk* chunk); +static cc_result AudioBase_AllocChunks(int size, struct AudioChunk* chunks, int numChunks); +static void AudioBase_FreeChunks(struct AudioChunk* chunks, int numChunks); + +/* achieve higher speed by playing samples at higher sample rate */ +#define Audio_AdjustSampleRate(sampleRate, playbackRate) ((sampleRate * playbackRate) / 100) + +#if defined CC_BUILD_OPENAL +/*########################################################################################################################* +*------------------------------------------------------OpenAL backend-----------------------------------------------------* +*#########################################################################################################################*/ +/* Simpler to just include subset of OpenAL actually use here instead of including */ +/* === BEGIN OPENAL HEADERS === */ +#if defined _WIN32 +#define APIENTRY __cdecl +#else +#define APIENTRY +#endif +#define AL_NONE 0 +#define AL_GAIN 0x100A +#define AL_SOURCE_STATE 0x1010 +#define AL_PLAYING 0x1012 +#define AL_BUFFERS_QUEUED 0x1015 +#define AL_BUFFERS_PROCESSED 0x1016 +#define AL_FORMAT_MONO16 0x1101 +#define AL_FORMAT_STEREO16 0x1103 + +#define AL_INVALID_NAME 0xA001 +#define AL_INVALID_ENUM 0xA002 +#define AL_INVALID_VALUE 0xA003 +#define AL_INVALID_OPERATION 0xA004 +#define AL_OUT_OF_MEMORY 0xA005 + +typedef char ALboolean; +typedef int ALint; +typedef unsigned int ALuint; +typedef int ALsizei; +typedef int ALenum; + +/* Apologies for the ugly dynamic symbol definitions here */ +static ALenum (APIENTRY *_alGetError)(void); +static void (APIENTRY *_alGenSources)(ALsizei n, ALuint* sources); +static void (APIENTRY *_alDeleteSources)(ALsizei n, const ALuint* sources); +static void (APIENTRY *_alGetSourcei)(ALuint source, ALenum param, ALint* value); +static void (APIENTRY *_alSourcef)(ALuint source, ALenum param, float value); +static void (APIENTRY *_alSourcePlay)(ALuint source); +static void (APIENTRY *_alSourceStop)(ALuint source); +static void (APIENTRY *_alSourceQueueBuffers)(ALuint source, ALsizei nb, const ALuint* buffers); +static void (APIENTRY *_alSourceUnqueueBuffers)(ALuint source, ALsizei nb, ALuint* buffers); +static void (APIENTRY *_alGenBuffers)(ALsizei n, ALuint* buffers); +static void (APIENTRY *_alDeleteBuffers)(ALsizei n, const ALuint* buffers); +static void (APIENTRY *_alBufferData)(ALuint buffer, ALenum format, const void* data, ALsizei size, ALsizei freq); + +static void (APIENTRY *_alDistanceModel)(ALenum distanceModel); +static void* (APIENTRY *_alcCreateContext)(void* device, const ALint* attrlist); +static ALboolean (APIENTRY *_alcMakeContextCurrent)(void* context); +static void (APIENTRY *_alcDestroyContext)(void* context); +static void* (APIENTRY *_alcOpenDevice)(const char* devicename); +static ALboolean (APIENTRY *_alcCloseDevice)(void* device); +static ALenum (APIENTRY *_alcGetError)(void* device); +/* === END OPENAL HEADERS === */ + +struct AudioContext { + ALuint source; + ALuint buffers[AUDIO_MAX_BUFFERS]; + ALuint freeIDs[AUDIO_MAX_BUFFERS]; + int count, free, sampleRate; + ALenum format; +}; +#define AUDIO_COMMON_ALLOC + +static void* audio_device; +static void* audio_context; + +#if defined CC_BUILD_WIN +static const cc_string alLib = String_FromConst("openal32.dll"); +#elif defined CC_BUILD_MACOS +static const cc_string alLib = String_FromConst("/System/Library/Frameworks/OpenAL.framework/Versions/A/OpenAL"); +#elif defined CC_BUILD_IOS +static const cc_string alLib = String_FromConst("/System/Library/Frameworks/OpenAL.framework/OpenAL"); +#elif defined CC_BUILD_NETBSD +static const cc_string alLib = String_FromConst("/usr/pkg/lib/libopenal.so"); +#elif defined CC_BUILD_BSD +static const cc_string alLib = String_FromConst("libopenal.so"); +#else +static const cc_string alLib = String_FromConst("libopenal.so.1"); +#endif + +static cc_bool LoadALFuncs(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(alcCreateContext), DynamicLib_Sym(alcMakeContextCurrent), + DynamicLib_Sym(alcDestroyContext), DynamicLib_Sym(alcOpenDevice), + DynamicLib_Sym(alcCloseDevice), DynamicLib_Sym(alcGetError), + + DynamicLib_Sym(alGetError), + DynamicLib_Sym(alGenSources), DynamicLib_Sym(alDeleteSources), + DynamicLib_Sym(alGetSourcei), DynamicLib_Sym(alSourcef), + DynamicLib_Sym(alSourcePlay), DynamicLib_Sym(alSourceStop), + DynamicLib_Sym(alSourceQueueBuffers), DynamicLib_Sym(alSourceUnqueueBuffers), + DynamicLib_Sym(alGenBuffers), DynamicLib_Sym(alDeleteBuffers), + DynamicLib_Sym(alBufferData), DynamicLib_Sym(alDistanceModel) + }; + void* lib; + + return DynamicLib_LoadAll(&alLib, funcs, Array_Elems(funcs), &lib); +} + +static cc_result CreateALContext(void) { + ALenum err; + audio_device = _alcOpenDevice(NULL); + if ((err = _alcGetError(audio_device))) return err; + if (!audio_device) return AL_ERR_INIT_DEVICE; + + audio_context = _alcCreateContext(audio_device, NULL); + if ((err = _alcGetError(audio_device))) return err; + if (!audio_context) return AL_ERR_INIT_CONTEXT; + + _alcMakeContextCurrent(audio_context); + return _alcGetError(audio_device); +} + +cc_bool AudioBackend_Init(void) { + static const cc_string msg = String_FromConst("Failed to init OpenAL. No audio will play."); + cc_result res; + if (audio_device) return true; + if (!LoadALFuncs()) { Logger_WarnFunc(&msg); return false; } + + res = CreateALContext(); + if (res) { Audio_Warn(res, "initing OpenAL"); return false; } + return true; +} + +void AudioBackend_Tick(void) { } + +void AudioBackend_Free(void) { + if (!audio_device) return; + _alcMakeContextCurrent(NULL); + + if (audio_context) _alcDestroyContext(audio_context); + if (audio_device) _alcCloseDevice(audio_device); + + audio_context = NULL; + audio_device = NULL; +} + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + ALenum i, err; + _alDistanceModel(AL_NONE); + ctx->source = 0; + ctx->count = buffers; + + _alGetError(); /* Reset error state */ + _alGenSources(1, &ctx->source); + if ((err = _alGetError())) return err; + + _alGenBuffers(buffers, ctx->buffers); + if ((err = _alGetError())) return err; + + for (i = 0; i < buffers; i++) { + ctx->freeIDs[i] = ctx->buffers[i]; + } + ctx->free = buffers; + return 0; +} + +static void Audio_Stop(struct AudioContext* ctx) { + _alSourceStop(ctx->source); +} + +static void Audio_Reset(struct AudioContext* ctx) { + _alDeleteSources(1, &ctx->source); + _alDeleteBuffers(ctx->count, ctx->buffers); + ctx->source = 0; +} + +static void ClearFree(struct AudioContext* ctx) { + int i; + for (i = 0; i < AUDIO_MAX_BUFFERS; i++) { + ctx->freeIDs[i] = 0; + } + ctx->free = 0; +} + +void Audio_Close(struct AudioContext* ctx) { + if (ctx->source) { + Audio_Stop(ctx); + Audio_Reset(ctx); + _alGetError(); /* Reset error state */ + } + ClearFree(ctx); + ctx->count = 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + ctx->sampleRate = Audio_AdjustSampleRate(sampleRate, playbackRate); + + if (channels == 1) { + ctx->format = AL_FORMAT_MONO16; + } else if (channels == 2) { + ctx->format = AL_FORMAT_STEREO16; + } else { + return ERR_INVALID_ARGUMENT; + } + return 0; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + _alSourcef(ctx->source, AL_GAIN, volume / 100.0f); + _alGetError(); /* Reset error state */ +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + ALuint buffer; + ALenum err; + + if (!ctx->free) return ERR_INVALID_ARGUMENT; + buffer = ctx->freeIDs[--ctx->free]; + _alGetError(); /* Reset error state */ + + _alBufferData(buffer, ctx->format, chunk->data, chunk->size, ctx->sampleRate); + if ((err = _alGetError())) return err; + _alSourceQueueBuffers(ctx->source, 1, &buffer); + if ((err = _alGetError())) return err; + return 0; +} + +cc_result Audio_Play(struct AudioContext* ctx) { + _alSourcePlay(ctx->source); + return _alGetError(); +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + ALint processed = 0; + ALuint buffer; + ALenum err; + + *inUse = 0; + if (!ctx->source) return 0; + + _alGetError(); /* Reset error state */ + _alGetSourcei(ctx->source, AL_BUFFERS_PROCESSED, &processed); + if ((err = _alGetError())) return err; + + if (processed > 0) { + _alSourceUnqueueBuffers(ctx->source, 1, &buffer); + if ((err = _alGetError())) return err; + + ctx->freeIDs[ctx->free++] = buffer; + } + *inUse = ctx->count - ctx->free; return 0; +} + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + /* Channels/Sample rate is per buffer, not a per source property */ + return true; +} + +static const char* GetError(cc_result res) { + switch (res) { + case AL_ERR_INIT_CONTEXT: return "Failed to init OpenAL context"; + case AL_ERR_INIT_DEVICE: return "Failed to init OpenAL device"; + case AL_INVALID_NAME: return "Invalid parameter name"; + case AL_INVALID_ENUM: return "Invalid parameter"; + case AL_INVALID_VALUE: return "Invalid parameter value"; + case AL_INVALID_OPERATION: return "Invalid operation"; + case AL_OUT_OF_MEMORY: return "OpenAL out of memory"; + } + return NULL; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + const char* err = GetError(res); + if (err) String_AppendConst(dst, err); + return err != NULL; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + return AudioBase_AllocChunks(size, chunks, numChunks); +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + AudioBase_FreeChunks(chunks, numChunks); +} +#elif defined CC_BUILD_WINMM +/*########################################################################################################################* +*------------------------------------------------------WinMM backend------------------------------------------------------* +*#########################################################################################################################*/ +#define WIN32_LEAN_AND_MEAN +#define NOSERVICE +#define NOMCX +#define NOIME +#ifndef UNICODE +#define UNICODE +#define _UNICODE +#endif +#include + +/* === BEGIN mmsyscom.h === */ +#define CALLBACK_NULL 0x00000000l +typedef UINT MMRESULT; +#define WINMMAPI DECLSPEC_IMPORT +#define MMSYSERR_BADDEVICEID 2 +/* === BEGIN mmeapi.h === */ +typedef struct WAVEHDR_ { + LPSTR lpData; + DWORD dwBufferLength; + DWORD dwBytesRecorded; + DWORD_PTR dwUser; + DWORD dwFlags; + DWORD dwLoops; + struct WAVEHDR_* lpNext; + DWORD_PTR reserved; +} WAVEHDR; + +typedef struct WAVEFORMATEX_ { + WORD wFormatTag; + WORD nChannels; + DWORD nSamplesPerSec; + DWORD nAvgBytesPerSec; + WORD nBlockAlign; + WORD wBitsPerSample; + WORD cbSize; +} WAVEFORMATEX; +typedef void* HWAVEOUT; + +#define WAVE_MAPPER ((UINT)-1) +#define WAVE_FORMAT_PCM 1 +#define WHDR_DONE 0x00000001 +#define WHDR_PREPARED 0x00000002 + +WINMMAPI MMRESULT WINAPI waveOutOpen(HWAVEOUT* phwo, UINT deviceID, const WAVEFORMATEX* fmt, DWORD_PTR callback, DWORD_PTR instance, DWORD flags); +WINMMAPI MMRESULT WINAPI waveOutClose(HWAVEOUT hwo); +WINMMAPI MMRESULT WINAPI waveOutPrepareHeader(HWAVEOUT hwo, WAVEHDR* hdr, UINT hdrSize); +WINMMAPI MMRESULT WINAPI waveOutUnprepareHeader(HWAVEOUT hwo, WAVEHDR* hdr, UINT hdrSize); +WINMMAPI MMRESULT WINAPI waveOutWrite(HWAVEOUT hwo, WAVEHDR* hdr, UINT hdrSize); +WINMMAPI MMRESULT WINAPI waveOutReset(HWAVEOUT hwo); +WINMMAPI MMRESULT WINAPI waveOutGetErrorTextA(MMRESULT err, LPSTR text, UINT textLen); +WINMMAPI UINT WINAPI waveOutGetNumDevs(void); +/* === END mmeapi.h === */ + +struct AudioContext { + HWAVEOUT handle; + WAVEHDR headers[AUDIO_MAX_BUFFERS]; + int count, channels, sampleRate, volume; + cc_uint32 _tmpSize[AUDIO_MAX_BUFFERS]; + void* _tmpData[AUDIO_MAX_BUFFERS]; +}; +#define AUDIO_COMMON_VOLUME +#define AUDIO_COMMON_ALLOC + +cc_bool AudioBackend_Init(void) { return true; } +void AudioBackend_Tick(void) { } +void AudioBackend_Free(void) { } + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + int i; + for (i = 0; i < buffers; i++) { + ctx->headers[i].dwFlags = WHDR_DONE; + } + ctx->count = buffers; + ctx->volume = 100; + return 0; +} + +static void Audio_Stop(struct AudioContext* ctx) { + waveOutReset(ctx->handle); +} + +static cc_result Audio_Reset(struct AudioContext* ctx) { + cc_result res; + if (!ctx->handle) return 0; + + res = waveOutClose(ctx->handle); + ctx->handle = NULL; + return res; +} + +void Audio_Close(struct AudioContext* ctx) { + int inUse; + if (ctx->handle) { + Audio_Stop(ctx); + Audio_Poll(ctx, &inUse); /* unprepare buffers */ + Audio_Reset(ctx); + } + AudioBase_Clear(ctx); +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + WAVEFORMATEX fmt; + cc_result res; + int sampleSize; + + sampleRate = Audio_AdjustSampleRate(sampleRate, playbackRate); + if (ctx->channels == channels && ctx->sampleRate == sampleRate) return 0; + ctx->channels = channels; + ctx->sampleRate = sampleRate; + + sampleSize = channels * 2; /* 16 bits per sample / 8 */ + if ((res = Audio_Reset(ctx))) return res; + + fmt.wFormatTag = WAVE_FORMAT_PCM; + fmt.nChannels = channels; + fmt.nSamplesPerSec = sampleRate; + fmt.nAvgBytesPerSec = sampleRate * sampleSize; + fmt.nBlockAlign = sampleSize; + fmt.wBitsPerSample = 16; + fmt.cbSize = 0; + + res = waveOutOpen(&ctx->handle, WAVE_MAPPER, &fmt, 0, 0, CALLBACK_NULL); + /* Show a better error message when no audio output devices connected than */ + /* "A device ID has been used that is out of range for your system" */ + if (res == MMSYSERR_BADDEVICEID && waveOutGetNumDevs() == 0) + return ERR_NO_AUDIO_OUTPUT; + return res; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { ctx->volume = volume; } + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + cc_result res; + WAVEHDR* hdr; + cc_bool ok; + int i; + struct AudioChunk tmp = *chunk; + + for (i = 0; i < ctx->count; i++) { + hdr = &ctx->headers[i]; + if (!(hdr->dwFlags & WHDR_DONE)) continue; + + ok = AudioBase_AdjustSound(ctx, i, &tmp); + if (!ok) return ERR_OUT_OF_MEMORY; + + Mem_Set(hdr, 0, sizeof(WAVEHDR)); + hdr->lpData = (LPSTR)tmp.data; + hdr->dwBufferLength = tmp.size; + hdr->dwLoops = 1; + + if ((res = waveOutPrepareHeader(ctx->handle, hdr, sizeof(WAVEHDR)))) return res; + if ((res = waveOutWrite(ctx->handle, hdr, sizeof(WAVEHDR)))) return res; + return 0; + } + /* tried to queue data without polling for free buffers first */ + return ERR_INVALID_ARGUMENT; +} + +cc_result Audio_Play(struct AudioContext* ctx) { return 0; } + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + cc_result res = 0; + WAVEHDR* hdr; + int i, count = 0; + + for (i = 0; i < ctx->count; i++) { + hdr = &ctx->headers[i]; + if (!(hdr->dwFlags & WHDR_DONE)) { count++; continue; } + + if (!(hdr->dwFlags & WHDR_PREPARED)) continue; + /* unprepare this WAVEHDR so it can be reused */ + res = waveOutUnprepareHeader(ctx->handle, hdr, sizeof(WAVEHDR)); + if (res) break; + } + + *inUse = count; return res; +} + + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + int channels = data->channels; + int sampleRate = Audio_AdjustSampleRate(data->sampleRate, data->rate); + return !ctx->channels || (ctx->channels == channels && ctx->sampleRate == sampleRate); +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + char buffer[NATIVE_STR_LEN] = { 0 }; + waveOutGetErrorTextA(res, buffer, NATIVE_STR_LEN); + + if (!buffer[0]) return false; + String_AppendConst(dst, buffer); + return true; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + return AudioBase_AllocChunks(size, chunks, numChunks); +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + AudioBase_FreeChunks(chunks, numChunks); +} +#elif defined CC_BUILD_OPENSLES +/*########################################################################################################################* +*----------------------------------------------------OpenSL ES backend----------------------------------------------------* +*#########################################################################################################################*/ +#include +#include +#include "ExtMath.h" +static SLObjectItf slEngineObject; +static SLEngineItf slEngineEngine; +static SLObjectItf slOutputObject; + +struct AudioContext { + int count, volume; + int channels, sampleRate; + SLObjectItf playerObject; + SLPlayItf playerPlayer; + SLBufferQueueItf playerQueue; + SLPlaybackRateItf playerRate; + SLVolumeItf playerVolume; +}; +#define AUDIO_COMMON_ALLOC + +static SLresult (SLAPIENTRY *_slCreateEngine)(SLObjectItf* engine, SLuint32 numOptions, const SLEngineOption* engineOptions, + SLuint32 numInterfaces, const SLInterfaceID* interfaceIds, const SLboolean* interfaceRequired); +static SLInterfaceID* _SL_IID_NULL; +static SLInterfaceID* _SL_IID_PLAY; +static SLInterfaceID* _SL_IID_ENGINE; +static SLInterfaceID* _SL_IID_BUFFERQUEUE; +static SLInterfaceID* _SL_IID_PLAYBACKRATE; +static SLInterfaceID* _SL_IID_VOLUME; +static const cc_string slLib = String_FromConst("libOpenSLES.so"); + +static cc_bool LoadSLFuncs(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(slCreateEngine), DynamicLib_Sym(SL_IID_NULL), + DynamicLib_Sym(SL_IID_PLAY), DynamicLib_Sym(SL_IID_ENGINE), + DynamicLib_Sym(SL_IID_BUFFERQUEUE), DynamicLib_Sym(SL_IID_PLAYBACKRATE), + DynamicLib_Sym(SL_IID_VOLUME) + }; + void* lib; + + return DynamicLib_LoadAll(&slLib, funcs, Array_Elems(funcs), &lib); +} + +cc_bool AudioBackend_Init(void) { + static const cc_string msg = String_FromConst("Failed to init OpenSLES. No audio will play."); + SLInterfaceID ids[1]; + SLboolean req[1]; + SLresult res; + + if (slEngineObject) return true; + if (!LoadSLFuncs()) { Logger_WarnFunc(&msg); return false; } + + /* mixer doesn't use any effects */ + ids[0] = *_SL_IID_NULL; req[0] = SL_BOOLEAN_FALSE; + + res = _slCreateEngine(&slEngineObject, 0, NULL, 0, NULL, NULL); + if (res) { Audio_Warn(res, "creating OpenSL ES engine"); return false; } + + res = (*slEngineObject)->Realize(slEngineObject, SL_BOOLEAN_FALSE); + if (res) { Audio_Warn(res, "realising OpenSL ES engine"); return false; } + + res = (*slEngineObject)->GetInterface(slEngineObject, *_SL_IID_ENGINE, &slEngineEngine); + if (res) { Audio_Warn(res, "initing OpenSL ES engine"); return false; } + + res = (*slEngineEngine)->CreateOutputMix(slEngineEngine, &slOutputObject, 1, ids, req); + if (res) { Audio_Warn(res, "creating OpenSL ES mixer"); return false; } + + res = (*slOutputObject)->Realize(slOutputObject, SL_BOOLEAN_FALSE); + if (res) { Audio_Warn(res, "realising OpenSL ES mixer"); return false; } + + return true; +} + +void AudioBackend_Tick(void) { } + +void AudioBackend_Free(void) { + if (slOutputObject) { + (*slOutputObject)->Destroy(slOutputObject); + slOutputObject = NULL; + } + if (slEngineObject) { + (*slEngineObject)->Destroy(slEngineObject); + slEngineObject = NULL; + slEngineEngine = NULL; + } +} + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + ctx->count = buffers; + ctx->volume = 100; + return 0; +} + +static void Audio_Stop(struct AudioContext* ctx) { + if (!ctx->playerPlayer) return; + + (*ctx->playerQueue)->Clear(ctx->playerQueue); + (*ctx->playerPlayer)->SetPlayState(ctx->playerPlayer, SL_PLAYSTATE_STOPPED); +} + +static void Audio_Reset(struct AudioContext* ctx) { + SLObjectItf playerObject = ctx->playerObject; + if (!playerObject) return; + + (*playerObject)->Destroy(playerObject); + ctx->playerObject = NULL; + ctx->playerPlayer = NULL; + ctx->playerQueue = NULL; + ctx->playerRate = NULL; + ctx->playerVolume = NULL; +} + +void Audio_Close(struct AudioContext* ctx) { + Audio_Stop(ctx); + Audio_Reset(ctx); + + ctx->count = 0; + ctx->channels = 0; + ctx->sampleRate = 0; +} + +static float Log10(float volume) { return Math_Log2(volume) / Math_Log2(10); } + +static void UpdateVolume(struct AudioContext* ctx) { + /* Object doesn't exist until Audio_SetFormat is called */ + if (!ctx->playerVolume) return; + + /* log of 0 is undefined */ + SLmillibel attenuation = ctx->volume == 0 ? SL_MILLIBEL_MIN : (2000 * Log10(ctx->volume / 100.0f)); + (*ctx->playerVolume)->SetVolumeLevel(ctx->playerVolume, attenuation); +} + +static cc_result RecreatePlayer(struct AudioContext* ctx, int channels, int sampleRate) { + SLDataLocator_AndroidSimpleBufferQueue input; + SLDataLocator_OutputMix output; + SLObjectItf playerObject; + SLDataFormat_PCM fmt; + SLInterfaceID ids[4]; + SLboolean req[4]; + SLDataSource src; + SLDataSink dst; + cc_result res; + + ctx->channels = channels; + ctx->sampleRate = sampleRate; + Audio_Reset(ctx); + + fmt.formatType = SL_DATAFORMAT_PCM; + fmt.numChannels = channels; + fmt.samplesPerSec = sampleRate * 1000; + fmt.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16; + fmt.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16; + fmt.channelMask = 0; + fmt.endianness = SL_BYTEORDER_LITTLEENDIAN; + + input.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE; + input.numBuffers = ctx->count; + output.locatorType = SL_DATALOCATOR_OUTPUTMIX; + output.outputMix = slOutputObject; + + src.pLocator = &input; + src.pFormat = &fmt; + dst.pLocator = &output; + dst.pFormat = NULL; + + ids[0] = *_SL_IID_BUFFERQUEUE; req[0] = SL_BOOLEAN_TRUE; + ids[1] = *_SL_IID_PLAY; req[1] = SL_BOOLEAN_TRUE; + ids[2] = *_SL_IID_PLAYBACKRATE; req[2] = SL_BOOLEAN_TRUE; + ids[3] = *_SL_IID_VOLUME; req[3] = SL_BOOLEAN_TRUE; + + res = (*slEngineEngine)->CreateAudioPlayer(slEngineEngine, &playerObject, &src, &dst, 4, ids, req); + ctx->playerObject = playerObject; + if (res) return res; + + if ((res = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE))) return res; + if ((res = (*playerObject)->GetInterface(playerObject, *_SL_IID_PLAY, &ctx->playerPlayer))) return res; + if ((res = (*playerObject)->GetInterface(playerObject, *_SL_IID_BUFFERQUEUE, &ctx->playerQueue))) return res; + if ((res = (*playerObject)->GetInterface(playerObject, *_SL_IID_PLAYBACKRATE, &ctx->playerRate))) return res; + if ((res = (*playerObject)->GetInterface(playerObject, *_SL_IID_VOLUME, &ctx->playerVolume))) return res; + + UpdateVolume(ctx); + return 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + cc_result res; + + if (ctx->channels != channels || ctx->sampleRate != sampleRate) { + if ((res = RecreatePlayer(ctx, channels, sampleRate))) return res; + } + + /* rate is in milli, so 1000 = normal rate */ + return (*ctx->playerRate)->SetRate(ctx->playerRate, playbackRate * 10); +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + ctx->volume = volume; + UpdateVolume(ctx); +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + return (*ctx->playerQueue)->Enqueue(ctx->playerQueue, chunk->data, chunk->size); +} + +cc_result Audio_Pause(struct AudioContext* ctx) { + return (*ctx->playerPlayer)->SetPlayState(ctx->playerPlayer, SL_PLAYSTATE_PAUSED); +} + +cc_result Audio_Play(struct AudioContext* ctx) { + return (*ctx->playerPlayer)->SetPlayState(ctx->playerPlayer, SL_PLAYSTATE_PLAYING); +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + SLBufferQueueState state = { 0 }; + cc_result res = 0; + + if (ctx->playerQueue) { + res = (*ctx->playerQueue)->GetState(ctx->playerQueue, &state); + } + *inUse = state.count; + return res; +} + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + return !ctx->channels || (ctx->channels == data->channels && ctx->sampleRate == data->sampleRate); +} + +static const char* GetError(cc_result res) { + switch (res) { + case SL_RESULT_PRECONDITIONS_VIOLATED: return "Preconditions violated"; + case SL_RESULT_PARAMETER_INVALID: return "Invalid parameter"; + case SL_RESULT_MEMORY_FAILURE: return "Memory failure"; + case SL_RESULT_RESOURCE_ERROR: return "Resource error"; + case SL_RESULT_RESOURCE_LOST: return "Resource lost"; + case SL_RESULT_IO_ERROR: return "I/O error"; + case SL_RESULT_BUFFER_INSUFFICIENT: return "Insufficient buffer"; + case SL_RESULT_CONTENT_CORRUPTED: return "Content corrupted"; + case SL_RESULT_CONTENT_UNSUPPORTED: return "Content unsupported"; + case SL_RESULT_CONTENT_NOT_FOUND: return "Content not found"; + case SL_RESULT_PERMISSION_DENIED: return "Permission denied"; + case SL_RESULT_FEATURE_UNSUPPORTED: return "Feature unsupported"; + case SL_RESULT_INTERNAL_ERROR: return "Internal error"; + case SL_RESULT_UNKNOWN_ERROR: return "Unknown error"; + case SL_RESULT_OPERATION_ABORTED: return "Operation aborted"; + case SL_RESULT_CONTROL_LOST: return "Control lost"; + } + return NULL; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + const char* err = GetError(res); + if (err) String_AppendConst(dst, err); + return err != NULL; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + return AudioBase_AllocChunks(size, chunks, numChunks); +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + AudioBase_FreeChunks(chunks, numChunks); +} +#elif defined CC_BUILD_3DS +/*########################################################################################################################* +*-------------------------------------------------------3DS backend-------------------------------------------------------* +*#########################################################################################################################*/ +#include <3ds.h> +struct AudioContext { + int chanID, count; + ndspWaveBuf bufs[AUDIO_MAX_BUFFERS]; + int sampleRate; + cc_bool stereo; +}; +static int channelIDs; + +// See https://github.com/devkitPro/3ds-examples/blob/master/audio/README.md +// To get audio to work in Citra, just create a 0 byte file in sdmc/3ds named dspfirm.cdca +cc_bool AudioBackend_Init(void) { + int result = ndspInit(); + Platform_Log2("NDSP_INIT: %i, %h", &result, &result); + + if (result == MAKERESULT(RL_PERMANENT, RS_NOTFOUND, RM_DSP, RD_NOT_FOUND)) { + static const cc_string msg = String_FromConst("/3ds/dspfirm.cdc not found on SD card, therefore no audio will play"); + Logger_WarnFunc(&msg); + } else if (result) { + Audio_Warn(result, "initing DSP for playing audio"); + } + + ndspSetOutputMode(NDSP_OUTPUT_STEREO); + return result == 0; +} + +void AudioBackend_Tick(void) { } + +void AudioBackend_Free(void) { } + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + int chanID = -1; + + for (int i = 0; i < 24; i++) + { + // channel in use + if (channelIDs & (1 << i)) continue; + + chanID = i; break; + } + if (chanID == -1) return ERR_INVALID_ARGUMENT; + + channelIDs |= (1 << chanID); + ctx->count = buffers; + ctx->chanID = chanID; + + ndspChnSetInterp(ctx->chanID, NDSP_INTERP_LINEAR); + return 0; +} + +void Audio_Close(struct AudioContext* ctx) { + if (ctx->count) { + ndspChnWaveBufClear(ctx->chanID); + channelIDs &= ~(1 << ctx->chanID); + } + ctx->count = 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + ctx->stereo = (channels == 2); + int fmt = ctx->stereo ? NDSP_FORMAT_STEREO_PCM16 : NDSP_FORMAT_MONO_PCM16; + + sampleRate = Audio_AdjustSampleRate(sampleRate, playbackRate); + ndspChnSetFormat(ctx->chanID, fmt); + ndspChnSetRate(ctx->chanID, sampleRate); + return 0; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + float mix[12] = { 0 }; + mix[0] = volume / 100.0f; + mix[1] = volume / 100.0f; + + ndspChnSetMix(ctx->chanID, mix); +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + ndspWaveBuf* buf; + + // DSP audio buffers must be aligned to a multiple of 0x80, according to the example code I could find. + if (((uintptr_t)chunk->data & 0x7F) != 0) { + Platform_Log1("Audio_QueueData: tried to queue buffer with non-aligned audio buffer 0x%x\n", &chunk->data); + } + if ((chunk->size & 0x7F) != 0) { + Platform_Log1("Audio_QueueData: unaligned audio data size 0x%x\n", &chunk->size); + } + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + if (buf->status == NDSP_WBUF_QUEUED || buf->status == NDSP_WBUF_PLAYING) + continue; + + buf->data_pcm16 = chunk->data; + buf->nsamples = chunk->size / (sizeof(cc_int16) * (ctx->stereo ? 2 : 1)); + DSP_FlushDataCache(buf->data_pcm16, chunk->size); + ndspChnWaveBufAdd(ctx->chanID, buf); + return 0; + } + // tried to queue data without polling for free buffers first + return ERR_INVALID_ARGUMENT; +} + +cc_result Audio_Play(struct AudioContext* ctx) { return 0; } + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + ndspWaveBuf* buf; + int count = 0; + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + if (buf->status == NDSP_WBUF_QUEUED || buf->status == NDSP_WBUF_PLAYING) { + count++; continue; + } + } + + *inUse = count; + return 0; +} + + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + return true; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + size = (size + 0x7F) & ~0x7F; // round up to nearest multiple of 0x80 + cc_uint8* dst = linearAlloc(size * numChunks); + if (!dst) return ERR_OUT_OF_MEMORY; + + for (int i = 0; i < numChunks; i++) + { + chunks[i].data = dst + size * i; + chunks[i].size = size; + } + return 0; +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + linearFree(chunks[0].data); +} +#elif defined CC_BUILD_SWITCH +/*########################################################################################################################* +*-----------------------------------------------------Switch backend------------------------------------------------------* +*#########################################################################################################################*/ +#include +#include +#include + +struct AudioContext { + int chanID, count; + AudioDriverWaveBuf bufs[AUDIO_MAX_BUFFERS]; + int channels, sampleRate; +}; + +static int channelIDs; +AudioDriver drv; +bool switchAudio = false; +void* audrv_mutex; + +cc_bool AudioBackend_Init(void) { + if (switchAudio) return true; + switchAudio = true; + + if (!audrv_mutex) audrv_mutex = Mutex_Create("Audio sync"); + + static const AudioRendererConfig arConfig = + { + .output_rate = AudioRendererOutputRate_48kHz, + .num_voices = 24, + .num_effects = 0, + .num_sinks = 1, + .num_mix_objs = 1, + .num_mix_buffers = 2, + }; + + audrenInitialize(&arConfig); + audrvCreate(&drv, &arConfig, 2); + + static const u8 sink_channels[] = { 0, 1 }; + /*int sink =*/ audrvDeviceSinkAdd(&drv, AUDREN_DEFAULT_DEVICE_NAME, 2, sink_channels); + + audrvUpdate(&drv); + + Result res = audrenStartAudioRenderer(); + + return R_SUCCEEDED(res); +} + +void AudioBackend_Tick(void) { + Mutex_Lock(audrv_mutex); + if (switchAudio) audrvUpdate(&drv); + Mutex_Unlock(audrv_mutex); +} + +void AudioBackend_Free(void) { + for (int i = 0; i < 24; i++) { + audrvVoiceStop(&drv, i); + } + audrvUpdate(&drv); +} + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + int chanID = -1; + + for (int i = 0; i < 24; i++) + { + // channel in use + if (channelIDs & (1 << i)) continue; + + chanID = i; break; + } + if (chanID == -1) return ERR_INVALID_ARGUMENT; + + channelIDs |= (1 << chanID); + ctx->count = buffers; + ctx->chanID = chanID; + return 0; +} + +void Audio_Close(struct AudioContext* ctx) { + if (ctx->count) { + audrvVoiceStop(&drv, ctx->chanID); + channelIDs &= ~(1 << ctx->chanID); + } + ctx->count = 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + sampleRate = Audio_AdjustSampleRate(sampleRate, playbackRate); + ctx->channels = channels; + ctx->sampleRate = sampleRate; + + audrvVoiceStop(&drv, ctx->chanID); + audrvVoiceInit(&drv, ctx->chanID, ctx->channels, PcmFormat_Int16, ctx->sampleRate); + audrvVoiceSetDestinationMix(&drv, ctx->chanID, AUDREN_FINAL_MIX_ID); + + if (channels == 1) { + // mono + audrvVoiceSetMixFactor(&drv, ctx->chanID, 1.0f, 0, 0); + audrvVoiceSetMixFactor(&drv, ctx->chanID, 1.0f, 0, 1); + } else { + // stereo + audrvVoiceSetMixFactor(&drv, ctx->chanID, 1.0f, 0, 0); + audrvVoiceSetMixFactor(&drv, ctx->chanID, 0.0f, 0, 1); + audrvVoiceSetMixFactor(&drv, ctx->chanID, 0.0f, 1, 0); + audrvVoiceSetMixFactor(&drv, ctx->chanID, 1.0f, 1, 1); + } + + return 0; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + audrvVoiceSetVolume(&drv, ctx->chanID, volume / 100.0f); +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + AudioDriverWaveBuf* buf; + + // Audio buffers must be aligned to a multiple of 0x1000, according to libnx example code + if (((uintptr_t)chunk->data & 0xFFF) != 0) { + Platform_Log1("Audio_QueueData: tried to queue buffer with non-aligned audio buffer 0x%x\n", &chunk->data); + } + if ((chunk->size & 0xFFF) != 0) { + Platform_Log1("Audio_QueueData: unaligned audio data size 0x%x\n", &chunk->size); + } + + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + int state = buf->state; + cc_uint32 endOffset = chunk->size / (sizeof(cc_int16) * ((ctx->channels == 2) ? 2 : 1)); + + if (state == AudioDriverWaveBufState_Queued || state == AudioDriverWaveBufState_Playing || state == AudioDriverWaveBufState_Waiting) + continue; + + buf->data_pcm16 = chunk->data; + buf->size = chunk->size; + buf->start_sample_offset = 0; + buf->end_sample_offset = endOffset; + + Mutex_Lock(audrv_mutex); + audrvVoiceAddWaveBuf(&drv, ctx->chanID, buf); + Mutex_Unlock(audrv_mutex); + + return 0; + } + + // tried to queue data without polling for free buffers first + return ERR_INVALID_ARGUMENT; +} + +cc_result Audio_Play(struct AudioContext* ctx) { + audrvVoiceStart(&drv, ctx->chanID); + return 0; +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + AudioDriverWaveBuf* buf; + int count = 0; + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + if (buf->state == AudioDriverWaveBufState_Queued || buf->state == AudioDriverWaveBufState_Playing || buf->state == AudioDriverWaveBufState_Waiting) { + count++; continue; + } + } + + *inUse = count; + return 0; +} + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + return true; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + size = (size + 0xFFF) & ~0xFFF; // round up to nearest multiple of 0x1000 + void* dst = aligned_alloc(0x1000, size * numChunks); + if (!dst) return ERR_OUT_OF_MEMORY; + + for (int i = 0; i < numChunks; i++) + { + chunks[i].data = dst + size * i; + chunks[i].size = size; + + int mpid = audrvMemPoolAdd(&drv, chunks[i].data, size); + audrvMemPoolAttach(&drv, mpid); + chunks[i].meta.val = mpid; + } + return 0; +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + for (int i = 0; i < numChunks; i++) + { + if (!chunks[i].data) continue; + int mpid = chunks[i].meta.val; + + audrvMemPoolDetach(&drv, mpid); + audrvMemPoolRemove(&drv, mpid); + } + free(chunks[0].data); +} +#elif defined CC_BUILD_GCWII +/*########################################################################################################################* +*-----------------------------------------------------GC/Wii backend------------------------------------------------------* +*#########################################################################################################################*/ +#include +#include +#include +#include + +struct AudioBuffer { + int available; + int size; + void* samples; +}; + +struct AudioContext { + int chanID, count, bufHead; + struct AudioBuffer bufs[AUDIO_MAX_BUFFERS]; + int channels, sampleRate, volume; + cc_bool makeAvailable; +}; + +cc_bool AudioBackend_Init(void) { + ASND_Init(); + ASND_Pause(0); + return true; +} + +void AudioBackend_Tick(void) { } + +void AudioBackend_Free(void) { + ASND_Pause(1); + ASND_End(); +} + +void MusicCallback(s32 voice) { + struct AudioContext* ctx = &music_ctx; + struct AudioBuffer* nextBuf = &ctx->bufs[(ctx->bufHead + 1) % ctx->count]; + + if (ASND_StatusVoice(voice) != SND_WORKING) return; + + if (ASND_AddVoice(voice, nextBuf->samples, nextBuf->size) == SND_OK) { + ctx->bufHead = (ctx->bufHead + 1) % ctx->count; + if (ctx->bufHead == 2) ctx->makeAvailable = true; + if (ctx->makeAvailable) { + int prev = ctx->bufHead - 2; + if (prev < 0) prev += 4; + ctx->bufs[prev].available = true; + } + } + + int inUse; + Audio_Poll(ctx, &inUse); + if (!inUse) { + // music has finished, stop the voice so this function isn't called anymore + ASND_StopVoice(ctx->chanID); + } +} + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + ctx->chanID = -1; + ctx->count = buffers; + ctx->volume = 255; + ctx->bufHead = 0; + ctx->makeAvailable = false; + + Mem_Set(ctx->bufs, 0, sizeof(ctx->bufs)); + for (int i = 0; i < buffers; i++) { + ctx->bufs[i].available = true; + } + + return 0; +} + +void Audio_Close(struct AudioContext* ctx) { + if (ctx->chanID != -1) ASND_StopVoice(ctx->chanID); + ctx->chanID = -1; + ctx->count = 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + sampleRate = Audio_AdjustSampleRate(sampleRate, playbackRate); + ctx->channels = channels; + ctx->sampleRate = sampleRate; + ctx->chanID = ASND_GetFirstUnusedVoice(); + + return 0; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + ctx->volume = (volume / 100.0f) * 255; +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + // Audio buffers must be aligned and padded to a multiple of 32 bytes + if (((uintptr_t)chunk->data & 0x1F) != 0) { + Platform_Log1("Audio_QueueData: tried to queue buffer with non-aligned audio buffer 0x%x\n", &chunk->data); + } + + struct AudioBuffer* buf; + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + if (!buf->available) continue; + + buf->samples = chunk->data; + buf->size = chunk->size; + buf->available = false; + + return 0; + } + + // tried to queue data without polling for free buffers first + return ERR_INVALID_ARGUMENT; +} + +cc_result Audio_Play(struct AudioContext* ctx) { + int format = (ctx->channels == 2) ? VOICE_STEREO_16BIT : VOICE_MONO_16BIT; + ASND_SetVoice(ctx->chanID, format, ctx->sampleRate, 0, ctx->bufs[0].samples, ctx->bufs[0].size, ctx->volume, ctx->volume, (ctx->count > 1) ? MusicCallback : NULL); + if (ctx->count == 1) ctx->bufs[0].available = true; + + return 0; +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + struct AudioBuffer* buf; + int count = 0; + + for (int i = 0; i < ctx->count; i++) { + buf = &ctx->bufs[i]; + if (!buf->available) count++; + } + + *inUse = count; + return 0; +} + + +cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + return true; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + size = (size + 0x1F) & ~0x1F; // round up to nearest multiple of 0x20 + void* dst = memalign(0x20, size * numChunks); + if (!dst) return ERR_OUT_OF_MEMORY; + + for (int i = 0; i < numChunks; i++) + { + chunks[i].data = dst + size * i; + chunks[i].size = size; + } + return 0; +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + free(chunks[0].data); +} +#elif defined CC_BUILD_DREAMCAST +/*########################################################################################################################* +*----------------------------------------------------Dreamcast backend----------------------------------------------------* +*#########################################################################################################################*/ +#include +/* TODO needs way more testing, especially with sounds */ +static cc_bool valid_handles[SND_STREAM_MAX]; + +struct AudioBuffer { + int available; + int bytesLeft; + void* samples; +}; + +struct AudioContext { + int bufHead, channels; + snd_stream_hnd_t hnd; + struct AudioBuffer bufs[AUDIO_MAX_BUFFERS]; + int count, sampleRate; +}; + +cc_bool AudioBackend_Init(void) { + return snd_stream_init() == 0; +} + +void AudioBackend_Tick(void) { + // TODO is this really threadsafe with music? should this be done in Audio_Poll instead? + for (int i = 0; i < SND_STREAM_MAX; i++) + { + if (valid_handles[i]) snd_stream_poll(i); + } +} + +void AudioBackend_Free(void) { + snd_stream_shutdown(); +} + +static void* AudioCallback(snd_stream_hnd_t hnd, int smp_req, int *smp_recv) { + struct AudioContext* ctx = snd_stream_get_userdata(hnd); + struct AudioBuffer* buf = &ctx->bufs[ctx->bufHead]; + + int samples = min(buf->bytesLeft, smp_req); + *smp_recv = samples; + void* ptr = buf->samples; + + buf->samples += samples; + buf->bytesLeft -= samples; + + if (buf->bytesLeft == 0) { + ctx->bufHead = (ctx->bufHead + 1) % ctx->count; + buf->samples = NULL; + buf->available = true; + + // special case to fix sounds looping + if (samples == 0 && ptr == NULL) *smp_recv = smp_req; + } + return ptr; +} + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + ctx->hnd = snd_stream_alloc(AudioCallback, SND_STREAM_BUFFER_MAX); + if (ctx->hnd == SND_STREAM_INVALID) return ERR_NOT_SUPPORTED; + snd_stream_set_userdata(ctx->hnd, ctx); + + Mem_Set(ctx->bufs, 0, sizeof(ctx->bufs)); + for (int i = 0; i < buffers; i++) { + ctx->bufs[i].available = true; + } + + ctx->count = buffers; + ctx->bufHead = 0; + valid_handles[ctx->hnd] = true; + return 0; +} + +void Audio_Close(struct AudioContext* ctx) { + if (ctx->count) { + snd_stream_stop(ctx->hnd); + snd_stream_destroy(ctx->hnd); + valid_handles[ctx->hnd] = false; + } + + ctx->hnd = SND_STREAM_INVALID; + ctx->count = 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + sampleRate = Audio_AdjustSampleRate(sampleRate, playbackRate); + ctx->channels = channels; + ctx->sampleRate = sampleRate; + return 0; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + snd_stream_volume(ctx->hnd, volume); +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + struct AudioBuffer* buf; + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + if (!buf->available) continue; + + buf->samples = chunk->data; + buf->bytesLeft = chunk->size; + buf->available = false; + return 0; + } + // tried to queue data without polling for free buffers first + return ERR_INVALID_ARGUMENT; +} + +cc_result Audio_Play(struct AudioContext* ctx) { + snd_stream_start(ctx->hnd, ctx->sampleRate, ctx->channels == 2); + return 0; +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + struct AudioBuffer* buf; + int count = 0; + + for (int i = 0; i < ctx->count; i++) + { + buf = &ctx->bufs[i]; + if (!buf->available) count++; + } + + *inUse = count; + return 0; +} + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + return true; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +static int totalSize; +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + size = (size + 0x1F) & ~0x1F; // round up to nearest multiple of 32 + void* dst = memalign(32, size * numChunks); + if (!dst) return ERR_OUT_OF_MEMORY; + totalSize += size * numChunks; + Platform_Log3("ALLOC: %i X %i (%i)", &size, &numChunks, &totalSize); + + for (int i = 0; i < numChunks; i++) + { + chunks[i].data = dst + size * i; + chunks[i].size = size; + } + return 0; +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + free(chunks[0].data); +} + +#elif defined CC_BUILD_WEBAUDIO +/*########################################################################################################################* +*-----------------------------------------------------WebAudio backend----------------------------------------------------* +*#########################################################################################################################*/ +struct AudioContext { int contextID, count, rate; void* data; }; +#define AUDIO_COMMON_ALLOC + +extern int interop_InitAudio(void); +extern int interop_AudioCreate(void); +extern void interop_AudioClose(int contextID); +extern int interop_AudioPlay(int contextID, const void* name, int rate); +extern int interop_AudioPoll(int contextID, int* inUse); +extern int interop_AudioVolume(int contextID, int volume); +extern int interop_AudioDescribe(int res, char* buffer, int bufferLen); + +cc_bool AudioBackend_Init(void) { + cc_result res = interop_InitAudio(); + if (res) { Audio_Warn(res, "initing WebAudio context"); return false; } + return true; +} + +void AudioBackend_Tick(void) { } +void AudioBackend_Free(void) { } + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + ctx->count = buffers; + ctx->contextID = interop_AudioCreate(); + ctx->data = NULL; + ctx->rate = 100; + return 0; +} + +void Audio_Close(struct AudioContext* ctx) { + if (ctx->contextID) interop_AudioClose(ctx->contextID); + ctx->contextID = 0; + ctx->count = 0; +} + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + ctx->rate = playbackRate; return 0; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { + interop_AudioVolume(ctx->contextID, volume); +} + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + ctx->data = chunk->data; return 0; +} + +cc_result Audio_Play(struct AudioContext* ctx) { + return interop_AudioPlay(ctx->contextID, ctx->data, ctx->rate); +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + return interop_AudioPoll(ctx->contextID, inUse); +} + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { + /* Channels/Sample rate is per buffer, not a per source property */ + return true; +} + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { + char buffer[NATIVE_STR_LEN]; + int len = interop_AudioDescribe(res, buffer, NATIVE_STR_LEN); + + String_AppendUtf8(dst, buffer, len); + return len > 0; +} + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + return AudioBase_AllocChunks(size, chunks, numChunks); +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { + AudioBase_FreeChunks(chunks, numChunks); +} +#else +/*########################################################################################################################* +*----------------------------------------------------Null/Empty backend---------------------------------------------------* +*#########################################################################################################################*/ +struct AudioContext { int count; }; + +cc_bool AudioBackend_Init(void) { return false; } +void AudioBackend_Tick(void) { } +void AudioBackend_Free(void) { } + +cc_result Audio_Init(struct AudioContext* ctx, int buffers) { + return ERR_NOT_SUPPORTED; +} + +void Audio_Close(struct AudioContext* ctx) { } + +cc_result Audio_SetFormat(struct AudioContext* ctx, int channels, int sampleRate, int playbackRate) { + return ERR_NOT_SUPPORTED; +} + +void Audio_SetVolume(struct AudioContext* ctx, int volume) { } + +cc_result Audio_QueueChunk(struct AudioContext* ctx, struct AudioChunk* chunk) { + return ERR_NOT_SUPPORTED; +} + +cc_result Audio_Play(struct AudioContext* ctx) { + return ERR_NOT_SUPPORTED; +} + +cc_result Audio_Poll(struct AudioContext* ctx, int* inUse) { + return ERR_NOT_SUPPORTED; +} + +static cc_bool Audio_FastPlay(struct AudioContext* ctx, struct AudioData* data) { return false; } + +cc_bool Audio_DescribeError(cc_result res, cc_string* dst) { return false; } + +cc_result Audio_AllocChunks(cc_uint32 size, struct AudioChunk* chunks, int numChunks) { + return ERR_NOT_SUPPORTED; +} + +void Audio_FreeChunks(struct AudioChunk* chunks, int numChunks) { } +#endif + + +/*########################################################################################################################* +*---------------------------------------------------Common backend code---------------------------------------------------* +*#########################################################################################################################*/ + +#ifdef AUDIO_COMMON_VOLUME +static void ApplyVolume(cc_int16* samples, int count, int volume) { + int i; + + for (i = 0; i < (count & ~0x07); i += 8, samples += 8) { + samples[0] = (samples[0] * volume / 100); + samples[1] = (samples[1] * volume / 100); + samples[2] = (samples[2] * volume / 100); + samples[3] = (samples[3] * volume / 100); + + samples[4] = (samples[4] * volume / 100); + samples[5] = (samples[5] * volume / 100); + samples[6] = (samples[6] * volume / 100); + samples[7] = (samples[7] * volume / 100); + } + + for (; i < count; i++, samples++) { + samples[0] = (samples[0] * volume / 100); + } +} + +static void AudioBase_Clear(struct AudioContext* ctx) { + int i; + ctx->count = 0; + ctx->channels = 0; + ctx->sampleRate = 0; + + for (i = 0; i < AUDIO_MAX_BUFFERS; i++) + { + Mem_Free(ctx->_tmpData[i]); + ctx->_tmpData[i] = NULL; + ctx->_tmpSize[i] = 0; + } +} + +static cc_bool AudioBase_AdjustSound(struct AudioContext* ctx, int i, struct AudioChunk* chunk) { + void* audio; + cc_uint32 src_size = chunk->size; + if (ctx->volume >= 100) return true; + + /* copy to temp buffer to apply volume */ + if (ctx->_tmpSize[i] < src_size) { + /* TODO: check if we can realloc NULL without a problem */ + if (ctx->_tmpData[i]) { + audio = Mem_TryRealloc(ctx->_tmpData[i], src_size, 1); + } else { + audio = Mem_TryAlloc(src_size, 1); + } + + if (!audio) return false; + ctx->_tmpData[i] = audio; + ctx->_tmpSize[i] = src_size; + } + + audio = ctx->_tmpData[i]; + Mem_Copy(audio, chunk->data, src_size); + ApplyVolume((cc_int16*)audio, src_size / 2, ctx->volume); + + chunk->data = audio; + return true; +} +#endif + +#ifdef AUDIO_COMMON_ALLOC +static cc_result AudioBase_AllocChunks(int size, struct AudioChunk* chunks, int numChunks) { + cc_uint8* dst = (cc_uint8*)Mem_TryAlloc(numChunks, size); + int i; + if (!dst) return ERR_OUT_OF_MEMORY; + + for (i = 0; i < numChunks; i++) + { + chunks[i].data = dst + size * i; + chunks[i].size = size; + } + return 0; +} + +static void AudioBase_FreeChunks(struct AudioChunk* chunks, int numChunks) { + Mem_Free(chunks[0].data); +} +#endif + + +/*########################################################################################################################* +*---------------------------------------------------Audio context code----------------------------------------------------* +*#########################################################################################################################*/ +struct AudioContext music_ctx; +#define POOL_MAX_CONTEXTS 8 +static struct AudioContext context_pool[POOL_MAX_CONTEXTS]; + +#ifndef CC_BUILD_NOSOUNDS +static cc_result PlayAudio(struct AudioContext* ctx, struct AudioData* data) { + cc_result res; + Audio_SetVolume(ctx, data->volume); + + if ((res = Audio_SetFormat(ctx, data->channels, data->sampleRate, data->rate))) return res; + if ((res = Audio_QueueChunk(ctx, &data->chunk))) return res; + if ((res = Audio_Play(ctx))) return res; + return 0; +} + +cc_result AudioPool_Play(struct AudioData* data) { + struct AudioContext* ctx; + int inUse, i; + cc_result res; + + /* Try to play on a context that doesn't need to be recreated */ + for (i = 0; i < POOL_MAX_CONTEXTS; i++) { + ctx = &context_pool[i]; + if (!ctx->count && (res = Audio_Init(ctx, 1))) return res; + + if ((res = Audio_Poll(ctx, &inUse))) return res; + if (inUse > 0) continue; + + if (!Audio_FastPlay(ctx, data)) continue; + return PlayAudio(ctx, data); + } + + /* Try again with all contexts, even if need to recreate one (expensive) */ + for (i = 0; i < POOL_MAX_CONTEXTS; i++) { + ctx = &context_pool[i]; + res = Audio_Poll(ctx, &inUse); + + if (res) return res; + if (inUse > 0) continue; + + return PlayAudio(ctx, data); + } + return 0; +} + +void AudioPool_Close(void) { + int i; + for (i = 0; i < POOL_MAX_CONTEXTS; i++) { + Audio_Close(&context_pool[i]); + } +} +#endif diff --git a/src/AxisLinesRenderer.c b/src/AxisLinesRenderer.c new file mode 100644 index 0000000..81eebc2 --- /dev/null +++ b/src/AxisLinesRenderer.c @@ -0,0 +1,95 @@ +#include "AxisLinesRenderer.h" +#include "Graphics.h" +#include "Game.h" +#include "SelectionBox.h" +#include "PackedCol.h" +#include "Camera.h" +#include "Event.h" +#include "Entity.h" +#include "ExtMath.h" + +cc_bool AxisLinesRenderer_Enabled; +static GfxResourceID axisLines_vb; +#define AXISLINES_NUM_VERTICES 12 +#define AXISLINES_THICKNESS (1.0f / 32.0f) +#define AXISLINES_LENGTH 3.0f + +void AxisLinesRenderer_Render(void) { + static const cc_uint8 indices[36] = { + 2,2,1, 2,2,3, 4,2,3, 4,2,1, /* X arrow */ + 1,2,2, 1,2,4, 3,2,4, 3,2,2, /* Z arrow */ + 1,2,3, 1,4,3, 3,4,1, 3,2,1, /* Y arrow */ + }; + static const PackedCol colors[] = { + PackedCol_Make(255, 0, 0, 255), /* Red */ + PackedCol_Make( 0, 0, 255, 255), /* Blue */ + PackedCol_Make( 0, 255, 0, 255), /* Green */ + }; + + struct VertexColoured* v; + Vec3 coords[5], pos, dirVector; + int i, count; + float axisLengthScale, axisThicknessScale; + struct Entity* e; + + if (!AxisLinesRenderer_Enabled) return; + /* Don't do it in a ContextRecreated handler, because we only want VB recreated if ShowAxisLines in on. */ + if (!axisLines_vb) { + axisLines_vb = Gfx_CreateDynamicVb(VERTEX_FORMAT_COLOURED, AXISLINES_NUM_VERTICES); + } + e = &Entities.CurPlayer->Base; + + if (Camera.Active->isThirdPerson) { + pos = e->Position; + axisLengthScale = 1; + axisThicknessScale = 1; + pos.y += 0.05f; + } else { + pos = Camera.CurrentPos; + dirVector = Vec3_GetDirVector(e->Yaw * MATH_DEG2RAD, e->Pitch * MATH_DEG2RAD); + Vec3_Mul1(&dirVector, &dirVector, 0.5f); + Vec3_Add(&pos, &dirVector, &pos); + axisLengthScale = 1.0f / 32.0f; + axisThicknessScale = 1.0f / 8.0f; + } + count = 12; + + Vec3_Add1(&coords[0], &pos, -AXISLINES_LENGTH * axisLengthScale); + Vec3_Add1(&coords[1], &pos, -AXISLINES_THICKNESS * axisThicknessScale); + coords[2] = pos; + Vec3_Add1(&coords[3], &pos, AXISLINES_THICKNESS * axisThicknessScale); + Vec3_Add1(&coords[4], &pos, AXISLINES_LENGTH * axisLengthScale); + + v = (struct VertexColoured*)Gfx_LockDynamicVb(axisLines_vb, + VERTEX_FORMAT_COLOURED, AXISLINES_NUM_VERTICES); + for (i = 0; i < count; i++, v++) + { + v->x = coords[indices[i*3 + 0]].x; + v->y = coords[indices[i*3 + 1]].y; + v->z = coords[indices[i*3 + 2]].z; + v->Col = colors[i >> 2]; + } + + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + Gfx_UnlockDynamicVb(axisLines_vb); + Gfx_DrawVb_IndexedTris(count); +} + + +/*########################################################################################################################* +*-----------------------------------------------AxisLinesRenderer component-----------------------------------------------* +*#########################################################################################################################*/ +static void OnContextLost(void* obj) { + Gfx_DeleteDynamicVb(&axisLines_vb); +} + +static void OnInit(void) { + Event_Register_(&GfxEvents.ContextLost, NULL, OnContextLost); +} + +static void OnFree(void) { OnContextLost(NULL); } + +struct IGameComponent AxisLinesRenderer_Component = { + OnInit, /* Init */ + OnFree, /* Free */ +}; diff --git a/src/AxisLinesRenderer.h b/src/AxisLinesRenderer.h new file mode 100644 index 0000000..2e913d5 --- /dev/null +++ b/src/AxisLinesRenderer.h @@ -0,0 +1,14 @@ +#ifndef CC_AXISLINESRENDERER_H +#define CC_AXISLINESRENDERER_H +#include "Core.h" +/* Renders 3 lines showing direction of each axis. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct IGameComponent; +extern struct IGameComponent AxisLinesRenderer_Component; +/* Whether the 3 axis lines should be rendered */ +extern cc_bool AxisLinesRenderer_Enabled; + +void AxisLinesRenderer_Render(void); +#endif diff --git a/src/Bitmap.c b/src/Bitmap.c new file mode 100644 index 0000000..bf2823a --- /dev/null +++ b/src/Bitmap.c @@ -0,0 +1,730 @@ +#include "Bitmap.h" +#include "Platform.h" +#include "ExtMath.h" +#include "Deflate.h" +#include "Logger.h" +#include "Stream.h" +#include "Errors.h" +#include "Utils.h" +#include "Funcs.h" + +BitmapCol BitmapColor_Offset(BitmapCol color, int rBy, int gBy, int bBy) { + int r, g, b; + r = BitmapCol_R(color) + rBy; Math_Clamp(r, 0, 255); + g = BitmapCol_G(color) + gBy; Math_Clamp(g, 0, 255); + b = BitmapCol_B(color) + bBy; Math_Clamp(b, 0, 255); + return BitmapColor_RGB(r, g, b); +} + +BitmapCol BitmapColor_Scale(BitmapCol a, float t) { + cc_uint8 R = (cc_uint8)(BitmapCol_R(a) * t); + cc_uint8 G = (cc_uint8)(BitmapCol_G(a) * t); + cc_uint8 B = (cc_uint8)(BitmapCol_B(a) * t); + return (a & BITMAPCOLOR_A_MASK) | BitmapColor_R_Bits(R) | BitmapColor_G_Bits(G) | BitmapColor_B_Bits(B); +} + +void Bitmap_UNSAFE_CopyBlock(int srcX, int srcY, int dstX, int dstY, + struct Bitmap* src, struct Bitmap* dst, int size) { + int x, y; + for (y = 0; y < size; y++) { + BitmapCol* srcRow = Bitmap_GetRow(src, srcY + y) + srcX; + BitmapCol* dstRow = Bitmap_GetRow(dst, dstY + y) + dstX; + for (x = 0; x < size; x++) { dstRow[x] = srcRow[x]; } + } +} + +void Bitmap_Allocate(struct Bitmap* bmp, int width, int height) { + bmp->width = width; bmp->height = height; + bmp->scan0 = (BitmapCol*)Mem_Alloc(width * height, 4, "bitmap data"); +} + +void Bitmap_TryAllocate(struct Bitmap* bmp, int width, int height) { + bmp->width = width; bmp->height = height; + bmp->scan0 = (BitmapCol*)Mem_TryAlloc(width * height, 4); +} + +void Bitmap_Scale(struct Bitmap* dst, struct Bitmap* src, + int srcX, int srcY, int srcWidth, int srcHeight) { + BitmapCol* dstRow; + BitmapCol* srcRow; + int x, y, width, height; + + width = dst->width; + height = dst->height; + + for (y = 0; y < height; y++) { + srcRow = Bitmap_GetRow(src, srcY + (y * srcHeight / height)); + dstRow = Bitmap_GetRow(dst, y); + + for (x = 0; x < width; x++) { + dstRow[x] = srcRow[srcX + (x * srcWidth / width)]; + } + } +} + + +/*########################################################################################################################* +*------------------------------------------------------PNG decoder--------------------------------------------------------* +*#########################################################################################################################*/ +#define PNG_IHDR_SIZE 13 +#define PNG_PALETTE 256 +#define PNG_FourCC(a, b, c, d) (((cc_uint32)a << 24) | ((cc_uint32)b << 16) | ((cc_uint32)c << 8) | (cc_uint32)d) + +enum PngColor { + PNG_COLOR_GRAYSCALE = 0, PNG_COLOR_RGB = 2, PNG_COLOR_INDEXED = 3, + PNG_COLOR_GRAYSCALE_A = 4, PNG_COLOR_RGB_A = 6 +}; + +enum PngFilter { + PNG_FILTER_NONE, PNG_FILTER_SUB, PNG_FILTER_UP, PNG_FILTER_AVERAGE, PNG_FILTER_PAETH +}; + +typedef void (*Png_RowExpander)(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst); +static const cc_uint8 pngSig[PNG_SIG_SIZE] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + +/* 5.2 PNG signature */ +cc_bool Png_Detect(const cc_uint8* data, cc_uint32 len) { + return len >= PNG_SIG_SIZE && Mem_Equal(data, pngSig, PNG_SIG_SIZE); +} + +/* 9 Filtering */ +/* 13.9 Filtering */ +static void Png_ReconstructFirst(cc_uint8 type, cc_uint8 bytesPerPixel, cc_uint8* line, cc_uint32 lineLen) { + /* First scanline is a special case, where all values in prior array are 0 */ + cc_uint32 i, j; + + switch (type) { + case PNG_FILTER_SUB: + for (i = bytesPerPixel, j = 0; i < lineLen; i++, j++) { + line[i] += line[j]; + } + return; + + case PNG_FILTER_AVERAGE: + for (i = bytesPerPixel, j = 0; i < lineLen; i++, j++) { + line[i] += (line[j] >> 1); + } + return; + + case PNG_FILTER_PAETH: + for (i = bytesPerPixel, j = 0; i < lineLen; i++, j++) { + line[i] += line[j]; + } + return; + } +} + +static void Png_Reconstruct(cc_uint8 type, cc_uint8 bytesPerPixel, cc_uint8* line, cc_uint8* prior, cc_uint32 lineLen) { + cc_uint32 i, j; + + switch (type) { + case PNG_FILTER_SUB: + for (i = bytesPerPixel, j = 0; i < lineLen; i++, j++) { + line[i] += line[j]; + } + return; + + case PNG_FILTER_UP: + for (i = 0; i < lineLen; i++) { + line[i] += prior[i]; + } + return; + + case PNG_FILTER_AVERAGE: + for (i = 0; i < bytesPerPixel; i++) { + line[i] += (prior[i] >> 1); + } + for (j = 0; i < lineLen; i++, j++) { + line[i] += ((prior[i] + line[j]) >> 1); + } + return; + + case PNG_FILTER_PAETH: + /* TODO: verify this is right */ + for (i = 0; i < bytesPerPixel; i++) { + line[i] += prior[i]; + } + for (j = 0; i < lineLen; i++, j++) { + cc_uint8 a = line[j], b = prior[i], c = prior[j]; + int p = a + b - c; + int pa = Math_AbsI(p - a); + int pb = Math_AbsI(p - b); + int pc = Math_AbsI(p - c); + + if (pa <= pb && pa <= pc) { line[i] += a; } + else if (pb <= pc) { line[i] += b; } + else { line[i] += c; } + } + return; + } +} + +#define Bitmap_Set(dst, r,g,b,a) dst = BitmapCol_Make(r, g, b, a); + +/* 7.2 Scanlines */ +#define PNG_Do_Grayscale(dstI, src, scale) rgb = (src) * scale; Bitmap_Set(dst[dstI], rgb, rgb, rgb, 255); +#define PNG_Do_Grayscale_8() rgb = src[0]; Bitmap_Set(*dst, rgb, rgb, rgb, 255); dst--; src -= 1; +#define PNG_Do_Grayscale_A__8() rgb = src[0]; Bitmap_Set(*dst, rgb, rgb, rgb, src[1]); dst--; src -= 2; +#define PNG_Do_RGB__8() Bitmap_Set(*dst, src[0], src[1], src[2], 255); dst--; src -= 3; +#define PNG_Do_RGB_A__8() Bitmap_Set(*dst, src[0], src[1], src[2], src[3]); dst++; src += 4; +#define PNG_Do_Palette__8() *dst-- = palette[*src--]; + +#define PNG_Mask_1(i) (7 - (i & 7)) +#define PNG_Mask_2(i) ((3 - (i & 3)) * 2) +#define PNG_Mask_4(i) ((1 - (i & 1)) * 4) +#define PNG_Get__1(i) ((src[i >> 3] >> PNG_Mask_1(i)) & 1) +#define PNG_Get__2(i) ((src[i >> 2] >> PNG_Mask_2(i)) & 3) +#define PNG_Get__4(i) ((src[i >> 1] >> PNG_Mask_4(i)) & 7) + +static void Png_Expand_GRAYSCALE_1(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + int i; cc_uint8 rgb; /* NOTE: not optimised*/ + for (i = width - 1; i >= 0; i--) { PNG_Do_Grayscale(i, PNG_Get__1(i), 255); } +} + +static void Png_Expand_GRAYSCALE_2(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + int i; cc_uint8 rgb; /* NOTE: not optimised */ + for (i = width - 1; i >= 0; i--) { PNG_Do_Grayscale(i, PNG_Get__2(i), 85); } +} + +static void Png_Expand_GRAYSCALE_4(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + int i; cc_uint8 rgb; + for (i = width - 1; i >= 0; i--) { PNG_Do_Grayscale(i, PNG_Get__4(i), 17); } +} + +static void Png_Expand_GRAYSCALE_8(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + cc_uint8 rgb; + src += (width - 1) * 2; + dst += (width - 1); + + for (; width >= 4; width -= 4) { + PNG_Do_Grayscale_8(); PNG_Do_Grayscale_8(); + PNG_Do_Grayscale_8(); PNG_Do_Grayscale_8(); + } + for (; width > 0; width--) { PNG_Do_Grayscale_8(); } +} + +static void Png_Expand_RGB_8(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + src += (width - 1) * 3; + dst += (width - 1); + + for (; width >= 4; width -= 4) { + PNG_Do_RGB__8(); PNG_Do_RGB__8(); + PNG_Do_RGB__8(); PNG_Do_RGB__8(); + } + for (; width > 0; width--) { PNG_Do_RGB__8(); } +} + +static void Png_Expand_INDEXED_1(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + int i; /* NOTE: not optimised */ + for (i = width - 1; i >= 0; i--) { dst[i] = palette[PNG_Get__1(i)]; } +} + +static void Png_Expand_INDEXED_2(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + int i; /* NOTE: not optimised */ + for (i = width - 1; i >= 0; i--) { dst[i] = palette[PNG_Get__2(i)]; } +} + +static void Png_Expand_INDEXED_4(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + int i; /* NOTE: not optimised */ + for (i = width - 1; i >= 0; i--) { dst[i] = palette[PNG_Get__4(i)]; } +} + +static void Png_Expand_INDEXED_8(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + src += (width - 1) * 1; + dst += (width - 1); + + for (; width >= 4; width -= 4) { + PNG_Do_Palette__8(); PNG_Do_Palette__8(); + PNG_Do_Palette__8(); PNG_Do_Palette__8(); + } + for (; width > 0; width--) { PNG_Do_Palette__8(); } +} + +static void Png_Expand_GRAYSCALE_A_8(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + cc_uint8 rgb; + src += (width - 1) * 2; + dst += (width - 1); + + for (; width >= 4; width -= 4) { + PNG_Do_Grayscale_A__8(); PNG_Do_Grayscale_A__8(); + PNG_Do_Grayscale_A__8(); PNG_Do_Grayscale_A__8(); + } + for (; width > 0; width--) { PNG_Do_Grayscale_A__8(); } +} + +static void Png_Expand_RGB_A_8(int width, BitmapCol* palette, cc_uint8* src, BitmapCol* dst) { + /* Processed in forward order */ + + for (; width >= 4; width -= 4) { + PNG_Do_RGB_A__8(); PNG_Do_RGB_A__8(); + PNG_Do_RGB_A__8(); PNG_Do_RGB_A__8(); + } + for (; width > 0; width--) { PNG_Do_RGB_A__8(); } +} + +static Png_RowExpander Png_GetExpander(cc_uint8 col, cc_uint8 bitsPerSample) { + switch (col) { + case PNG_COLOR_GRAYSCALE: + switch (bitsPerSample) { + case 1: return Png_Expand_GRAYSCALE_1; + case 2: return Png_Expand_GRAYSCALE_2; + case 4: return Png_Expand_GRAYSCALE_4; + case 8: return Png_Expand_GRAYSCALE_8; + } + return NULL; + + case PNG_COLOR_RGB: + switch (bitsPerSample) { + case 8: return Png_Expand_RGB_8; + } + return NULL; + + case PNG_COLOR_INDEXED: + switch (bitsPerSample) { + case 1: return Png_Expand_INDEXED_1; + case 2: return Png_Expand_INDEXED_2; + case 4: return Png_Expand_INDEXED_4; + case 8: return Png_Expand_INDEXED_8; + } + return NULL; + + case PNG_COLOR_GRAYSCALE_A: + switch (bitsPerSample) { + case 8: return Png_Expand_GRAYSCALE_A_8; + } + return NULL; + + case PNG_COLOR_RGB_A: + switch (bitsPerSample) { + case 8: return Png_Expand_RGB_A_8; + } + return NULL; + } + return NULL; +} + +/* Sets alpha to 0 for any pixels in the bitmap whose RGB is same as colorspace */ +static void ComputeTransparency(struct Bitmap* bmp, BitmapCol col) { + BitmapCol trnsRGB = col & BITMAPCOLOR_RGB_MASK; + int x, y, width = bmp->width, height = bmp->height; + + for (y = 0; y < height; y++) { + BitmapCol* row = Bitmap_GetRow(bmp, y); + for (x = 0; x < width; x++) { + BitmapCol rgb = row[x] & BITMAPCOLOR_RGB_MASK; + row[x] = (rgb == trnsRGB) ? trnsRGB : row[x]; + } + } +} + +cc_result Png_Decode(struct Bitmap* bmp, struct Stream* stream) { + cc_uint8 tmp[64]; + cc_uint32 dataSize, fourCC; + cc_result res; + + /* header variables */ + static const cc_uint8 samplesPerPixel[] = { 1, 0, 3, 1, 2, 0, 4 }; + cc_uint8 colorspace, bitsPerSample, bytesPerPixel = 0; + Png_RowExpander rowExpander = NULL; + cc_uint32 scanlineSize = 0, scanlineBytes = 0; + + /* palette data */ + BitmapCol trnsColor; + BitmapCol palette[PNG_PALETTE]; + cc_uint32 i; + + /* idat state */ + cc_uint32 available = 0, rowY = 0; + cc_uint8 buffer[PNG_PALETTE * 3]; + cc_uint32 read, bufferIdx = 0; + cc_uint32 left, bufferLen = 0; + int curY; + + /* idat decompressor */ + struct InflateState inflate; + struct Stream compStream, datStream; + struct ZLibHeader zlibHeader; + cc_uint8* data = NULL; + + bmp->width = 0; bmp->height = 0; + bmp->scan0 = NULL; + + res = Stream_Read(stream, tmp, PNG_SIG_SIZE); + if (res) return res; + if (!Png_Detect(tmp, PNG_SIG_SIZE)) return PNG_ERR_INVALID_SIG; + + colorspace = 0xFF; /* Unknown colour space */ + trnsColor = BITMAPCOLOR_BLACK; + for (i = 0; i < PNG_PALETTE; i++) { palette[i] = BITMAPCOLOR_BLACK; } + + Inflate_MakeStream2(&compStream, &inflate, stream); + ZLibHeader_Init(&zlibHeader); + + for (;;) { + res = Stream_Read(stream, tmp, 8); + if (res) return res; + dataSize = Stream_GetU32_BE(tmp + 0); + fourCC = Stream_GetU32_BE(tmp + 4); + + switch (fourCC) { + /* 11.2.2 IHDR Image header */ + case PNG_FourCC('I','H','D','R'): { + if (dataSize != PNG_IHDR_SIZE) return PNG_ERR_INVALID_HDR_SIZE; + res = Stream_Read(stream, tmp, PNG_IHDR_SIZE); + if (res) return res; + + bmp->width = (int)Stream_GetU32_BE(tmp + 0); + bmp->height = (int)Stream_GetU32_BE(tmp + 4); + if (bmp->width < 0 || bmp->width > PNG_MAX_DIMS) return PNG_ERR_TOO_WIDE; + if (bmp->height < 0 || bmp->height > PNG_MAX_DIMS) return PNG_ERR_TOO_TALL; + + bitsPerSample = tmp[8]; colorspace = tmp[9]; + if (bitsPerSample == 16) return PNG_ERR_16BITSAMPLES; + + rowExpander = Png_GetExpander(colorspace, bitsPerSample); + if (!rowExpander) return PNG_ERR_INVALID_COL_BPP; + + if (tmp[10] != 0) return PNG_ERR_COMP_METHOD; + if (tmp[11] != 0) return PNG_ERR_FILTER; + if (tmp[12] != 0) return PNG_ERR_INTERLACED; + + bytesPerPixel = ((samplesPerPixel[colorspace] * bitsPerSample) + 7) >> 3; + scanlineSize = ((samplesPerPixel[colorspace] * bitsPerSample * bmp->width) + 7) >> 3; + scanlineBytes = scanlineSize + 1; /* Add 1 byte for filter byte of each scanline */ + + data = Mem_TryAlloc(bmp->height, max(scanlineBytes, bmp->width * 4)); + bmp->scan0 = (BitmapCol*)data; + if (!bmp->scan0) return ERR_OUT_OF_MEMORY; + + bufferLen = bmp->height * scanlineBytes; + } break; + + /* 11.2.3 PLTE Palette */ + case PNG_FourCC('P','L','T','E'): { + if (dataSize > PNG_PALETTE * 3) return PNG_ERR_PAL_SIZE; + if ((dataSize % 3) != 0) return PNG_ERR_PAL_SIZE; + + res = Stream_Read(stream, buffer, dataSize); + if (res) return res; + + for (i = 0; i < dataSize; i += 3) { + palette[i / 3] &= BITMAPCOLOR_A_MASK; /* set RGB to 0 */ + palette[i / 3] |= buffer[i ] << BITMAPCOLOR_R_SHIFT; + palette[i / 3] |= buffer[i + 1] << BITMAPCOLOR_G_SHIFT; + palette[i / 3] |= buffer[i + 2] << BITMAPCOLOR_B_SHIFT; + } + } break; + + /* 11.3.2.1 tRNS Transparency */ + case PNG_FourCC('t','R','N','S'): { + if (colorspace == PNG_COLOR_GRAYSCALE) { + if (dataSize != 2) return PNG_ERR_TRANS_COUNT; + + res = Stream_Read(stream, buffer, dataSize); + if (res) return res; + + /* RGB is always two bytes */ + trnsColor = BitmapCol_Make(buffer[1], buffer[1], buffer[1], 0); + } else if (colorspace == PNG_COLOR_INDEXED) { + if (dataSize > PNG_PALETTE) return PNG_ERR_TRANS_COUNT; + + res = Stream_Read(stream, buffer, dataSize); + if (res) return res; + + /* set alpha component of palette */ + for (i = 0; i < dataSize; i++) { + palette[i] &= BITMAPCOLOR_RGB_MASK; /* set A to 0 */ + palette[i] |= buffer[i] << BITMAPCOLOR_A_SHIFT; + } + } else if (colorspace == PNG_COLOR_RGB) { + if (dataSize != 6) return PNG_ERR_TRANS_COUNT; + + res = Stream_Read(stream, buffer, dataSize); + if (res) return res; + + /* R,G,B are always two bytes */ + trnsColor = BitmapCol_Make(buffer[1], buffer[3], buffer[5], 0); + } else { + return PNG_ERR_TRANS_INVALID; + } + } break; + + /* 11.2.4 IDAT Image data */ + case PNG_FourCC('I','D','A','T'): { + Stream_ReadonlyPortion(&datStream, stream, dataSize); + inflate.Source = &datStream; + + /* TODO: This assumes zlib header will be in 1 IDAT chunk */ + while (!zlibHeader.done) { + if ((res = ZLibHeader_Read(&datStream, &zlibHeader))) return res; + } + + if (!bmp->scan0) return PNG_ERR_NO_DATA; + if (rowY >= bmp->height) break; + left = bufferLen - bufferIdx; + + res = compStream.Read(&compStream, &data[bufferIdx], left, &read); + if (res) return res; + if (!read) break; + + available += read; + bufferIdx += read; + + /* Process all of the scanline(s) that have been fully decompressed */ + /* NOTE: Need to check height too, in case IDAT is corrupted and has extra data */ + for (; available >= scanlineBytes && rowY < bmp->height; rowY++, available -= scanlineBytes) { + cc_uint8* scanline = &data[rowY * scanlineBytes]; + if (scanline[0] > PNG_FILTER_PAETH) return PNG_ERR_INVALID_SCANLINE; + + if (rowY == 0) { + /* First row, prior is assumed as 0 */ + Png_ReconstructFirst(scanline[0], bytesPerPixel, &scanline[1], scanlineSize); + } else { + cc_uint8* prior = &data[(rowY - 1) * scanlineBytes]; + Png_Reconstruct(scanline[0], bytesPerPixel, &scanline[1], &prior[1], scanlineSize); + + /* With the RGBA colourspace, each scanline is (1 + width*4) bytes wide */ + /* Therefore once a row has been reconstructed, the prior row can be converted */ + /* immediately into the destination colour format */ + if (colorspace == PNG_COLOR_RGB_A) { + /* Prior line is no longer needed and can be overwritten now */ + rowExpander(bmp->width, palette, &prior[1], Bitmap_GetRow(bmp, rowY - 1)); + } + } + + /* Current line is also no longer needed and can be overwritten now */ + if (colorspace == PNG_COLOR_RGB_A && rowY == bmp->height - 1) { + rowExpander(bmp->width, palette, &scanline[1], Bitmap_GetRow(bmp, rowY)); + } + } + + /* Check if image fully decoded or not */ + if (bufferIdx != bufferLen) break; + + /* With other colourspaces, the length of a scanline might be less than the width of a 8bpp image row */ + /* Therefore image expansion can only be done after all the rows have been reconstructed, and must */ + /* be done backwards to avoid overwriting any source data that has yet to be processed */ + /* This is slightly slower, but the majority of images ClassiCube encounters are RGBA anyways */ + if (colorspace != PNG_COLOR_RGB_A) { + for (curY = bmp->height - 1; curY >= 0; curY--) { + cc_uint8* scanline = &data[curY * scanlineBytes]; + rowExpander(bmp->width, palette, &scanline[1], Bitmap_GetRow(bmp, curY)); + } + } + + if (!BitmapCol_A(trnsColor)) ComputeTransparency(bmp, trnsColor); + return 0; + } break; + + /* 11.2.5 IEND Image trailer */ + case PNG_FourCC('I','E','N','D'): + /* Reading all image data should be handled by above if in the IDAT chunk */ + /* If we reached here, it means not all of the image data was read */ + return PNG_ERR_REACHED_IEND; + + default: + if ((res = stream->Skip(stream, dataSize))) return res; + break; + } + + if ((res = stream->Skip(stream, 4))) return res; /* Skip CRC32 */ + } +} + + +/*########################################################################################################################* +*------------------------------------------------------PNG encoder--------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_FILESYSTEM +static void Png_Filter(cc_uint8 filter, const cc_uint8* cur, const cc_uint8* prior, cc_uint8* best, int lineLen, int bpp) { + /* 3 bytes per pixel constant */ + cc_uint8 a, b, c; + int i, p, pa, pb, pc; + + switch (filter) { + case PNG_FILTER_SUB: + for (i = 0; i < bpp; i++) { best[i] = cur[i]; } + + for (; i < lineLen; i++) { + best[i] = cur[i] - cur[i - bpp]; + } + break; + + case PNG_FILTER_UP: + for (i = 0; i < lineLen; i++) { + best[i] = cur[i] - prior[i]; + } + break; + + case PNG_FILTER_AVERAGE: + for (i = 0; i < bpp; i++) { best[i] = cur[i] - (prior[i] >> 1); } + + for (; i < lineLen; i++) { + best[i] = cur[i] - ((prior[i] + cur[i - bpp]) >> 1); + } + break; + + case PNG_FILTER_PAETH: + for (i = 0; i < bpp; i++) { best[i] = cur[i] - prior[i]; } + + for (; i < lineLen; i++) { + a = cur[i - bpp]; b = prior[i]; c = prior[i - bpp]; + p = a + b - c; + + pa = Math_AbsI(p - a); + pb = Math_AbsI(p - b); + pc = Math_AbsI(p - c); + + if (pa <= pb && pa <= pc) { best[i] = cur[i] - a; } + else if (pb <= pc) { best[i] = cur[i] - b; } + else { best[i] = cur[i] - c; } + } + break; + } +} + +static void Png_MakeRow(const BitmapCol* src, cc_uint8* dst, int lineLen, cc_bool alpha) { + cc_uint8* end = dst + lineLen; + BitmapCol col; /* if we use *src, register gets reloaded each time */ + + if (alpha) { + for (; dst < end; src++, dst += 4) { + col = *src; + dst[0] = BitmapCol_R(col); dst[1] = BitmapCol_G(col); + dst[2] = BitmapCol_B(col); dst[3] = BitmapCol_A(col); + } + } else { + for (; dst < end; src++, dst += 3) { + col = *src; + dst[0] = BitmapCol_R(col); dst[1] = BitmapCol_G(col); + dst[2] = BitmapCol_B(col); + } + } +} + +static void Png_EncodeRow(const cc_uint8* cur, const cc_uint8* prior, cc_uint8* best, int lineLen, cc_bool alpha) { + cc_uint8* dst; + int bestFilter = PNG_FILTER_SUB; + int bestEstimate = Int32_MaxValue; + int x, filter, estimate; + + dst = best + 1; + /* NOTE: Waste of time trying the PNG_NONE filter */ + for (filter = PNG_FILTER_SUB; filter <= PNG_FILTER_PAETH; filter++) { + Png_Filter(filter, cur, prior, dst, lineLen, alpha ? 4 : 3); + + /* Estimate how well this filtered line will compress, based on */ + /* smallest sum of magnitude of each byte (signed) in the line */ + /* (see note in PNG specification, 12.8 "Filter selection" ) */ + estimate = 0; + for (x = 0; x < lineLen; x++) { + estimate += Math_AbsI((cc_int8)dst[x]); + } + + if (estimate > bestEstimate) continue; + bestEstimate = estimate; + bestFilter = filter; + } + + /* The bytes in dst are from last filter run (paeth) */ + /* However, we want dst to be bytes from the best filter */ + if (bestFilter != PNG_FILTER_PAETH) { + Png_Filter(bestFilter, cur, prior, dst, lineLen, alpha ? 4 : 3); + } + + best[0] = bestFilter; +} + +static BitmapCol* DefaultGetRow(struct Bitmap* bmp, int y, void* ctx) { return Bitmap_GetRow(bmp, y); } +static cc_result Png_EncodeCore(struct Bitmap* bmp, struct Stream* stream, cc_uint8* buffer, + Png_RowGetter getRow, cc_bool alpha, void* ctx) { + cc_uint8 tmp[32]; + cc_uint8* prevLine = buffer; + cc_uint8* curLine = buffer + (bmp->width * 4) * 1; + cc_uint8* bestLine = buffer + (bmp->width * 4) * 2; + + struct ZLibState zlState; + struct Stream chunk, zlStream; + cc_uint32 stream_end, stream_beg; + int y, lineSize; + cc_result res; + + /* stream may not start at 0 (e.g. when making default.zip) */ + if ((res = stream->Position(stream, &stream_beg))) return res; + + if (!getRow) getRow = DefaultGetRow; + if ((res = Stream_Write(stream, pngSig, PNG_SIG_SIZE))) return res; + Stream_WriteonlyCrc32(&chunk, stream); + + /* Write header chunk */ + Stream_SetU32_BE(&tmp[0], PNG_IHDR_SIZE); + Stream_SetU32_BE(&tmp[4], PNG_FourCC('I','H','D','R')); + { + Stream_SetU32_BE(&tmp[8], bmp->width); + Stream_SetU32_BE(&tmp[12], bmp->height); + tmp[16] = 8; /* bits per sample */ + tmp[17] = alpha ? PNG_COLOR_RGB_A : PNG_COLOR_RGB; + tmp[18] = 0; /* DEFLATE compression method */ + tmp[19] = 0; /* ADAPTIVE filter method */ + tmp[20] = 0; /* Not using interlacing */ + } + Stream_SetU32_BE(&tmp[21], Utils_CRC32(&tmp[4], 17)); + + /* Write PNG body */ + Stream_SetU32_BE(&tmp[25], 0); /* size of IDAT, filled in later */ + if ((res = Stream_Write(stream, tmp, 29))) return res; + Stream_SetU32_BE(&tmp[0], PNG_FourCC('I','D','A','T')); + if ((res = Stream_Write(&chunk, tmp, 4))) return res; + + ZLib_MakeStream(&zlStream, &zlState, &chunk); + lineSize = bmp->width * (alpha ? 4 : 3); + Mem_Set(prevLine, 0, lineSize); + + for (y = 0; y < bmp->height; y++) { + BitmapCol* src = getRow(bmp, y, ctx); + cc_uint8* prev = (y & 1) == 0 ? prevLine : curLine; + cc_uint8* cur = (y & 1) == 0 ? curLine : prevLine; + + Png_MakeRow(src, cur, lineSize, alpha); + Png_EncodeRow(cur, prev, bestLine, lineSize, alpha); + + /* +1 for filter byte */ + if ((res = Stream_Write(&zlStream, bestLine, lineSize + 1))) return res; + } + if ((res = zlStream.Close(&zlStream))) return res; + Stream_SetU32_BE(&tmp[0], chunk.meta.crc32.crc32 ^ 0xFFFFFFFFUL); + + /* Write end chunk */ + Stream_SetU32_BE(&tmp[4], 0); + Stream_SetU32_BE(&tmp[8], PNG_FourCC('I','E','N','D')); + Stream_SetU32_BE(&tmp[12], 0xAE426082UL); /* CRC32 of IEND */ + if ((res = Stream_Write(stream, tmp, 16))) return res; + + /* Come back to fixup size of data in data chunk */ + if ((res = stream->Position(stream, &stream_end))) return res; + if ((res = stream->Seek(stream, stream_beg + 33))) return res; + + Stream_SetU32_BE(&tmp[0], (stream_end - stream_beg) - 57); + if ((res = Stream_Write(stream, tmp, 4))) return res; + return stream->Seek(stream, stream_end); +} + +cc_result Png_Encode(struct Bitmap* bmp, struct Stream* stream, + Png_RowGetter getRow, cc_bool alpha, void* ctx) { + cc_result res; + /* Add 1 for scanline filter type byter */ + cc_uint8* buffer = Mem_TryAlloc(3, bmp->width * 4 + 1); + if (!buffer) return ERR_NOT_SUPPORTED; + + res = Png_EncodeCore(bmp, stream, buffer, getRow, alpha, ctx); + Mem_Free(buffer); + return res; +} +#else +/* No point including encoding code when can't save screenshots anyways */ +cc_result Png_Encode(struct Bitmap* bmp, struct Stream* stream, + Png_RowGetter getRow, cc_bool alpha, void* ctx) { + return ERR_NOT_SUPPORTED; +} +#endif + diff --git a/src/Bitmap.h b/src/Bitmap.h new file mode 100644 index 0000000..ea00b1c --- /dev/null +++ b/src/Bitmap.h @@ -0,0 +1,104 @@ +#ifndef CC_BITMAP_H +#define CC_BITMAP_H +#include "Core.h" +/* Represents a 2D array of pixels. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Stream; + +/* Represents a packed 32 bit RGBA colour, suitable for native graphics API texture pixels. */ +typedef cc_uint32 BitmapCol; +#if defined CC_BUILD_WEB || defined CC_BUILD_ANDROID || defined CC_BUILD_PSP || defined CC_BUILD_PSVITA || defined CC_BUILD_PS2 + #define BITMAPCOLOR_R_SHIFT 0 + #define BITMAPCOLOR_G_SHIFT 8 + #define BITMAPCOLOR_B_SHIFT 16 + #define BITMAPCOLOR_A_SHIFT 24 +#elif defined CC_BUILD_3DS || defined CC_BUILD_N64 || defined CC_BUILD_WIIU + #define BITMAPCOLOR_R_SHIFT 24 + #define BITMAPCOLOR_G_SHIFT 16 + #define BITMAPCOLOR_B_SHIFT 8 + #define BITMAPCOLOR_A_SHIFT 0 +#else + #define BITMAPCOLOR_B_SHIFT 0 + #define BITMAPCOLOR_G_SHIFT 8 + #define BITMAPCOLOR_R_SHIFT 16 + #define BITMAPCOLOR_A_SHIFT 24 +#endif + +#define BITMAPCOLOR_R_MASK (0xFFU << BITMAPCOLOR_R_SHIFT) +#define BITMAPCOLOR_G_MASK (0xFFU << BITMAPCOLOR_G_SHIFT) +#define BITMAPCOLOR_B_MASK (0xFFU << BITMAPCOLOR_B_SHIFT) +#define BITMAPCOLOR_A_MASK (0xFFU << BITMAPCOLOR_A_SHIFT) + +/* Extracts just the R/G/B/A component from a bitmap color */ +#define BitmapCol_R(color) ((cc_uint8)(color >> BITMAPCOLOR_R_SHIFT)) +#define BitmapCol_G(color) ((cc_uint8)(color >> BITMAPCOLOR_G_SHIFT)) +#define BitmapCol_B(color) ((cc_uint8)(color >> BITMAPCOLOR_B_SHIFT)) +#define BitmapCol_A(color) ((cc_uint8)(color >> BITMAPCOLOR_A_SHIFT)) + +#define BitmapColor_R_Bits(color) ((cc_uint8)(color) << BITMAPCOLOR_R_SHIFT) +#define BitmapColor_G_Bits(color) ((cc_uint8)(color) << BITMAPCOLOR_G_SHIFT) +#define BitmapColor_B_Bits(color) ((cc_uint8)(color) << BITMAPCOLOR_B_SHIFT) +#define BitmapColor_A_Bits(color) ((cc_uint8)(color) << BITMAPCOLOR_A_SHIFT) + +#define BitmapCol_Make(r, g, b, a) (BitmapColor_R_Bits(r) | BitmapColor_G_Bits(g) | BitmapColor_B_Bits(b) | BitmapColor_A_Bits(a)) +#define BitmapColor_RGB(r, g, b) (BitmapColor_R_Bits(r) | BitmapColor_G_Bits(g) | BitmapColor_B_Bits(b) | BITMAPCOLOR_A_MASK) +#define BITMAPCOLOR_RGB_MASK (BITMAPCOLOR_R_MASK | BITMAPCOLOR_G_MASK | BITMAPCOLOR_B_MASK) + +#define BITMAPCOLOR_BLACK BitmapColor_RGB( 0, 0, 0) +#define BITMAPCOLOR_WHITE BitmapColor_RGB(255, 255, 255) + +BitmapCol BitmapColor_Offset(BitmapCol color, int rBy, int gBy, int bBy); +BitmapCol BitmapColor_Scale(BitmapCol a, float t); + +/* A 2D array of BitmapCol pixels */ +struct Bitmap { BitmapCol* scan0; int width, height; }; +/* Returns number of bytes a bitmap consumes. */ +#define Bitmap_DataSize(width, height) ((cc_uint32)(width) * (cc_uint32)(height) * 4) +/* Gets the yth row of the given bitmap. */ +#define Bitmap_GetRow(bmp, y) ((bmp)->scan0 + (y) * (bmp)->width) +/* Gets the pixel at (x,y) in the given bitmap. */ +/* NOTE: Does NOT check coordinates are inside the bitmap. */ +#define Bitmap_GetPixel(bmp, x, y) (Bitmap_GetRow(bmp, y)[x]) + +/* Initialises a bitmap instance. */ +#define Bitmap_Init(bmp, width_, height_, scan0_) bmp.width = width_; bmp.height = height_; bmp.scan0 = scan0_; +/* Copies a rectangle of pixels from one bitmap to another. */ +/* NOTE: If src and dst are the same, src and dst rectangles MUST NOT overlap. */ +/* NOTE: Rectangles are NOT checked for whether they lie inside the bitmaps. */ +void Bitmap_UNSAFE_CopyBlock(int srcX, int srcY, int dstX, int dstY, + struct Bitmap* src, struct Bitmap* dst, int size); +/* Allocates a new bitmap of the given dimensions. */ +/* NOTE: You are responsible for freeing its memory! */ +void Bitmap_Allocate(struct Bitmap* bmp, int width, int height); +/* Attemps to allocates a new bitmap of the given dimensions. */ +/* NOTE: You are responsible for freeing its memory! */ +void Bitmap_TryAllocate(struct Bitmap* bmp, int width, int height); +/* Scales a region of the source bitmap to occupy the entirety of the destination bitmap. */ +/* The pixels from the region are scaled upwards or downwards depending on destination width and height. */ +CC_API void Bitmap_Scale(struct Bitmap* dst, struct Bitmap* src, + int srcX, int srcY, int srcWidth, int srcHeight); + +#define PNG_SIG_SIZE 8 +#if defined CC_BUILD_LOWMEM +/* No point supporting > 1K x 1K bitmaps when system has less than 64 MB of RAM anyways */ +#define PNG_MAX_DIMS 1024 +#else +#define PNG_MAX_DIMS 0x8000 +#endif + +/* Whether data starts with PNG format signature/identifier. */ +cc_bool Png_Detect(const cc_uint8* data, cc_uint32 len); +typedef BitmapCol* (*Png_RowGetter)(struct Bitmap* bmp, int row, void* ctx); +/* + Decodes a bitmap in PNG format. Partially based off information from + https://handmade.network/forums/wip/t/2363-implementing_a_basic_png_reader_the_handmade_way + https://github.com/nothings/stb/blob/master/stb_image.h +*/ +CC_API cc_result Png_Decode(struct Bitmap* bmp, struct Stream* stream); +/* Encodes a bitmap in PNG format. */ +/* getRow is optional. Can be used to modify how rows are encoded. (e.g. flip image) */ +/* if alpha is non-zero, RGBA channels are saved, otherwise only RGB channels are. */ +cc_result Png_Encode(struct Bitmap* bmp, struct Stream* stream, + Png_RowGetter getRow, cc_bool alpha, void* ctx); +#endif diff --git a/src/Block.c b/src/Block.c new file mode 100644 index 0000000..d8eda91 --- /dev/null +++ b/src/Block.c @@ -0,0 +1,805 @@ +#include "Block.h" +#include "Funcs.h" +#include "ExtMath.h" +#include "TexturePack.h" +#include "Game.h" +#include "Entity.h" +#include "Inventory.h" +#include "Event.h" +#include "Picking.h" +#include "Lighting.h" + +struct _BlockLists Blocks; + +const char* const Sound_Names[SOUND_COUNT] = { + "none", "wood", "gravel", "grass", "stone", + "metal", "glass", "cloth", "sand", "snow", +}; + +/*########################################################################################################################* +*---------------------------------------------------Default properties----------------------------------------------------* +*#########################################################################################################################*/ +#define FOG_NONE 0 +#define FOG_WATER PackedCol_Make( 5, 5, 51, 255) +#define FOG_LAVA PackedCol_Make(153, 25, 0, 255) + +/* Brightness */ +#define BRIT_NONE 0 +#define BRIT_FULL FANCY_LIGHTING_MAX_LEVEL +#define BRIT_MAGM 10 + +struct SimpleBlockDef { + const char* name; + cc_uint8 topTexture, sideTexture, bottomTexture, height; + PackedCol fogColor; cc_uint8 fogDensity; + cc_uint8 brightness, blocksLight; cc_uint8 gravity; + cc_uint8 draw, collide, digSound, stepSound; +}; +static const struct SimpleBlockDef invalid_blockDef = { + "Invalid", 0,0,0,16, FOG_NONE,0, false,true, 100, DRAW_OPAQUE,COLLIDE_SOLID,0 +}; + +/* Properties for all built-in blocks (Classic and CPE blocks) */ +static const struct SimpleBlockDef core_blockDefs[] = { +/*NAME TOP SID BOT HEI FOG_COLOR DENS BRIT BLOCKS GRAV DRAW_MODE COLLIDE_MODE DIG_SOUND STEP_SOUND */ +{ "Air", 0, 0, 0, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_GAS, COLLIDE_NONE, SOUND_NONE, SOUND_NONE }, +{ "Stone", 1, 1, 1, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Grass", 0, 3, 2, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_GRASS, SOUND_GRASS }, +{ "Dirt", 2, 2, 2, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_GRAVEL, SOUND_GRAVEL }, +{ "Cobblestone", 16, 16, 16, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Wood", 4, 4, 4, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_WOOD, SOUND_WOOD }, +{ "Sapling", 15, 15, 15, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_SPRITE, COLLIDE_NONE, SOUND_GRASS, SOUND_NONE }, +{ "Bedrock", 17, 17, 17, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, + +{ "Water", 14, 14, 14, 16, FOG_WATER, 10, BRIT_NONE, true, 100, DRAW_TRANSLUCENT, COLLIDE_WATER, SOUND_NONE, SOUND_NONE }, +{ "Still water", 14, 14, 14, 16, FOG_WATER, 10, BRIT_NONE, true, 100, DRAW_TRANSLUCENT, COLLIDE_WATER, SOUND_NONE, SOUND_NONE }, +{ "Lava", 30, 30, 30, 16, FOG_LAVA , 180, BRIT_FULL, true, 100, DRAW_OPAQUE, COLLIDE_LAVA, SOUND_NONE, SOUND_NONE }, +{ "Still lava", 30, 30, 30, 16, FOG_LAVA , 180, BRIT_FULL, true, 100, DRAW_OPAQUE, COLLIDE_LAVA, SOUND_NONE, SOUND_NONE }, +{ "Sand", 18, 18, 18, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_SAND, SOUND_SAND }, +{ "Gravel", 19, 19, 19, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_GRAVEL, SOUND_GRAVEL }, +{ "Gold ore", 32, 32, 32, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Iron ore", 33, 33, 33, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, + +{ "Coal ore", 34, 34, 34, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Log", 21, 20, 21, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_WOOD, SOUND_WOOD }, +{ "Leaves", 22, 22, 22, 16, FOG_NONE , 0, BRIT_NONE, false, 40, DRAW_TRANSPARENT_THICK, COLLIDE_SOLID, SOUND_GRASS, SOUND_GRASS }, +{ "Sponge", 48, 48, 48, 16, FOG_NONE , 0, BRIT_NONE, true, 90, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_GRASS, SOUND_GRASS }, +{ "Glass", 49, 49, 49, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_TRANSPARENT, COLLIDE_SOLID, SOUND_GLASS,SOUND_STONE}, +{ "Red", 64, 64, 64, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Orange", 65, 65, 65, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Yellow", 66, 66, 66, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, + +{ "Lime", 67, 67, 67, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Green", 68, 68, 68, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Teal", 69, 69, 69, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Aqua", 70, 70, 70, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Cyan", 71, 71, 71, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Blue", 72, 72, 72, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Indigo", 73, 73, 73, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Violet", 74, 74, 74, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, + +{ "Magenta", 75, 75, 75, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Pink", 76, 76, 76, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Black", 77, 77, 77, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Gray", 78, 78, 78, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "White", 79, 79, 79, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Dandelion", 13, 13, 13, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_SPRITE, COLLIDE_NONE, SOUND_GRASS, SOUND_NONE }, +{ "Rose", 12, 12, 12, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_SPRITE, COLLIDE_NONE, SOUND_GRASS, SOUND_NONE }, +{ "Brown mushroom", 29, 29, 29, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_SPRITE, COLLIDE_NONE, SOUND_GRASS, SOUND_NONE }, + +{ "Red mushroom", 28, 28, 28, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_SPRITE, COLLIDE_NONE, SOUND_GRASS, SOUND_NONE }, +{ "Gold", 24, 40, 56, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_METAL, SOUND_METAL }, +{ "Iron", 23, 39, 55, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_METAL, SOUND_METAL }, +{ "Double slab", 6, 5, 6, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Slab", 6, 5, 6, 8, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Brick", 7, 7, 7, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "TNT", 9, 8, 10, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_GRASS, SOUND_GRASS }, +{ "Bookshelf", 4, 35, 4, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_WOOD, SOUND_WOOD }, + +{ "Mossy rocks", 36, 36, 36, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Obsidian", 37, 37, 37, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Cobblestone slab", 16, 16, 16, 8, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Rope", 11, 11, 11, 16, FOG_NONE , 0, BRIT_NONE, false, 100, DRAW_SPRITE, COLLIDE_CLIMB, SOUND_CLOTH, SOUND_CLOTH }, +{ "Sandstone", 25, 41, 57, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Snow", 50, 50, 50, 4, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_NONE, SOUND_SNOW, SOUND_SNOW }, +{ "Fire", 38, 38, 38, 16, FOG_NONE , 0, BRIT_FULL, false, 100, DRAW_SPRITE, COLLIDE_NONE, SOUND_WOOD, SOUND_NONE }, +{ "Light pink", 80, 80, 80, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, + +{ "Forest green", 81, 81, 81, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Brown", 82, 82, 82, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Deep blue", 83, 83, 83, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Turquoise", 84, 84, 84, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_CLOTH, SOUND_CLOTH }, +{ "Ice", 51, 51, 51, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_TRANSLUCENT, COLLIDE_ICE, SOUND_STONE, SOUND_STONE }, +{ "Ceramic tile", 54, 54, 54, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Magma", 86, 86, 86, 16, FOG_NONE , 0, BRIT_MAGM, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, +{ "Pillar", 26, 42, 58, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE }, + +{ "Crate", 53, 53, 53, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_WOOD, SOUND_WOOD }, +{ "Stone brick", 52, 52, 52, 16, FOG_NONE , 0, BRIT_NONE, true, 100, DRAW_OPAQUE, COLLIDE_SOLID, SOUND_STONE, SOUND_STONE } +/*NAME TOP SID BOT HEI FOG_COLOR DENS BRIT BLOCKS GRAV DRAW_MODE COLLIDE_MODE DIG_SOUND STEP_SOUND */ +}; + +/* Returns a backwards compatible collide type of a block */ +static cc_uint8 DefaultSet_MapOldCollide(BlockID b, cc_uint8 collide) { + if (b == BLOCK_ROPE && collide == COLLIDE_NONE) return COLLIDE_CLIMB; + if (b == BLOCK_ICE && collide == COLLIDE_SOLID) return COLLIDE_ICE; + + if ((b == BLOCK_WATER || b == BLOCK_STILL_WATER) && collide == COLLIDE_LIQUID) + return COLLIDE_WATER; + if ((b == BLOCK_LAVA || b == BLOCK_STILL_LAVA) && collide == COLLIDE_LIQUID) + return COLLIDE_LAVA; + return collide; +} + + +/*########################################################################################################################* +*---------------------------------------------------Block properties------------------------------------------------------* +*#########################################################################################################################*/ +static void Block_RecalcIsLiquid(BlockID b) { + cc_uint8 collide = Blocks.ExtendedCollide[b]; + Blocks.IsLiquid[b] = + (collide == COLLIDE_WATER && Blocks.Draw[b] == DRAW_TRANSLUCENT) || + (collide == COLLIDE_LAVA && Blocks.Draw[b] == DRAW_TRANSPARENT); +} + +/* Sets the basic and extended collide types of the given block */ +static void Block_SetCollide(BlockID block, cc_uint8 collide) { + Blocks.ExtendedCollide[block] = collide; + Block_RecalcIsLiquid(block); + + /* Reduce extended collision types to their simpler forms */ + if (collide == COLLIDE_ICE) collide = COLLIDE_SOLID; + if (collide == COLLIDE_SLIPPERY_ICE) collide = COLLIDE_SOLID; + + if (collide == COLLIDE_WATER) collide = COLLIDE_LIQUID; + if (collide == COLLIDE_LAVA) collide = COLLIDE_LIQUID; + Blocks.Collide[block] = collide; +} + +/* Sets draw type and updates related state (e.g. FullOpaque) for the given block */ +static void Block_SetDrawType(BlockID block, cc_uint8 draw) { + if (draw == DRAW_OPAQUE && Blocks.Collide[block] != COLLIDE_SOLID) draw = DRAW_TRANSPARENT; + Blocks.Draw[block] = draw; + Block_RecalcIsLiquid(block); + + /* Whether a block is opaque and exactly occupies a cell in the world */ + /* The mesh builder module needs this information for optimisation purposes */ + Blocks.FullOpaque[block] = draw == DRAW_OPAQUE + && Blocks.MinBB[block].x == 0 && Blocks.MinBB[block].y == 0 && Blocks.MinBB[block].z == 0 + && Blocks.MaxBB[block].x == 1 && Blocks.MaxBB[block].y == 1 && Blocks.MaxBB[block].z == 1; +} + +void Block_SetSide(TextureLoc texLoc, BlockID blockId) { + int index = blockId * FACE_COUNT; + Blocks.Textures[index + FACE_XMIN] = texLoc; + Blocks.Textures[index + FACE_XMAX] = texLoc; + Blocks.Textures[index + FACE_ZMIN] = texLoc; + Blocks.Textures[index + FACE_ZMAX] = texLoc; +} + + +/*########################################################################################################################* +*--------------------------------------------------Block bounds/culling---------------------------------------------------* +*#########################################################################################################################*/ +/* Calculates render min/max corners of this block */ +/* Works by slightly offsetting collision min/max corners */ +static void Block_CalcRenderBounds(BlockID block) { + Vec3 min = Blocks.MinBB[block], max = Blocks.MaxBB[block]; + + if (Blocks.IsLiquid[block]) { + min.x += 0.1f/16.0f; max.x += 0.1f/16.0f; + min.z += 0.1f/16.0f; max.z += 0.1f/16.0f; + min.y -= 1.5f/16.0f; max.y -= 1.5f/16.0f; + } else if (Blocks.Draw[block] == DRAW_TRANSLUCENT && Blocks.Collide[block] != COLLIDE_SOLID) { + min.x += 0.1f/16.0f; max.x += 0.1f/16.0f; + min.z += 0.1f/16.0f; max.z += 0.1f/16.0f; + min.y -= 0.1f/16.0f; max.y -= 0.1f/16.0f; + } + + Blocks.RenderMinBB[block] = min; Blocks.RenderMaxBB[block] = max; +} + +/* Calculates light colour offset for each face of the given block */ +static void Block_CalcLightOffset(BlockID block) { + int flags = 0xFF; + Vec3 min = Blocks.MinBB[block], max = Blocks.MaxBB[block]; + + if (min.x != 0) flags &= ~(1 << FACE_XMIN); + if (max.x != 1) flags &= ~(1 << FACE_XMAX); + if (min.z != 0) flags &= ~(1 << FACE_ZMIN); + if (max.z != 1) flags &= ~(1 << FACE_ZMAX); + if (min.y != 0) flags &= ~(1 << FACE_YMIN); + if (max.y != 1) flags &= ~(1 << FACE_YMAX); + + if ((min.y != 0 && max.y == 1) && Blocks.Draw[block] != DRAW_GAS) { + flags &= ~(1 << LIGHT_FLAG_SHADES_FROM_BELOW); + } + Blocks.LightOffset[block] = flags; +} + + +static float GetSpriteBB_MinX(int size, int tileX, int tileY, const struct Bitmap* bmp) { + BitmapCol* row; + int x, y; + + for (x = 0; x < size; x++) { + for (y = 0; y < size; y++) { + row = Bitmap_GetRow(bmp, tileY * size + y) + (tileX * size); + if (BitmapCol_A(row[x])) { return (float)x / size; } + } + } + return 1.0f; +} + +static float GetSpriteBB_MinY(int size, int tileX, int tileY, const struct Bitmap* bmp) { + BitmapCol* row; + int x, y; + + for (y = size - 1; y >= 0; y--) { + row = Bitmap_GetRow(bmp, tileY * size + y) + (tileX * size); + for (x = 0; x < size; x++) { + if (BitmapCol_A(row[x])) { return 1.0f - (float)(y + 1) / size; } + } + } + return 1.0f; +} + +static float GetSpriteBB_MaxX(int size, int tileX, int tileY, const struct Bitmap* bmp) { + BitmapCol* row; + int x, y; + + for (x = size - 1; x >= 0; x--) { + for (y = 0; y < size; y++) { + row = Bitmap_GetRow(bmp, tileY * size + y) + (tileX * size); + if (BitmapCol_A(row[x])) { return (float)(x + 1) / size; } + } + } + return 0.0f; +} + +static float GetSpriteBB_MaxY(int size, int tileX, int tileY, const struct Bitmap* bmp) { + BitmapCol* row; + int x, y; + + for (y = 0; y < size; y++) { + row = Bitmap_GetRow(bmp, tileY * size + y) + (tileX * size); + for (x = 0; x < size; x++) { + if (BitmapCol_A(row[x])) { return 1.0f - (float)y / size; } + } + } + return 0.0f; +} + +/* Recalculates bounding box of the given sprite block */ +static void Block_RecalculateBB(BlockID block) { + struct Bitmap* bmp = &Atlas2D.Bmp; + int tileSize = Atlas2D.TileSize; + TextureLoc texLoc = Block_Tex(block, FACE_XMAX); + int x = Atlas2D_TileX(texLoc), y = Atlas2D_TileY(texLoc); + + Vec3 centre = { 0.5f, 0.0f, 0.5f }; + float minX = 0, minY = 0, maxX = 1, maxY = 1; + Vec3 minRaw, maxRaw; + + if (y < Atlas2D.RowsCount) { + minX = GetSpriteBB_MinX(tileSize, x, y, bmp); + minY = GetSpriteBB_MinY(tileSize, x, y, bmp); + maxX = GetSpriteBB_MaxX(tileSize, x, y, bmp); + maxY = GetSpriteBB_MaxY(tileSize, x, y, bmp); + } + + minRaw = Vec3_RotateY3(minX - 0.5f, minY, 0.0f, 45.0f * MATH_DEG2RAD); + maxRaw = Vec3_RotateY3(maxX - 0.5f, maxY, 0.0f, 45.0f * MATH_DEG2RAD); + Vec3_Add(&Blocks.MinBB[block], &minRaw, ¢re); + Vec3_Add(&Blocks.MaxBB[block], &maxRaw, ¢re); + Block_CalcRenderBounds(block); +} + +/* Recalculates bounding boxes of all sprite blocks */ +static void Block_RecalculateAllSpriteBB(void) { + int block; + for (block = BLOCK_AIR; block < BLOCK_COUNT; block++) { + if (Blocks.Draw[block] != DRAW_SPRITE) continue; + + Block_RecalculateBB((BlockID)block); + } +} + + +static void Block_CalcStretch(BlockID block) { + /* faces which can be stretched on X axis */ + if (Blocks.MinBB[block].x == 0.0f && Blocks.MaxBB[block].x == 1.0f) { + Blocks.CanStretch[block] |= 0x3C; + } else { + Blocks.CanStretch[block] &= 0xC3; /* ~0x3C */ + } + + /* faces which can be stretched on Z axis */ + if (Blocks.MinBB[block].z == 0.0f && Blocks.MaxBB[block].z == 1.0f) { + Blocks.CanStretch[block] |= 0x03; + } else { + Blocks.CanStretch[block] &= 0xFC; /* ~0x03 */ + } +} + +static cc_bool Block_MightCull(BlockID block, BlockID other) { + cc_uint8 bType, oType; + /* Sprite blocks can never cull blocks. */ + if (Blocks.Draw[block] == DRAW_SPRITE) return false; + + /* NOTE: Water is always culled by lava */ + if ((block == BLOCK_WATER || block == BLOCK_STILL_WATER) + && (other == BLOCK_LAVA || other == BLOCK_STILL_LAVA)) + return true; + + /* All blocks (except for say leaves) cull with themselves */ + if (block == other) return Blocks.Draw[block] != DRAW_TRANSPARENT_THICK; + + /* An opaque neighbour (asides from lava) culls this block. */ + if (Blocks.Draw[other] == DRAW_OPAQUE && !Blocks.IsLiquid[other]) return true; + /* Transparent/Gas blocks don't cull other blocks (except themselves) */ + if (Blocks.Draw[block] != DRAW_TRANSLUCENT || Blocks.Draw[other] != DRAW_TRANSLUCENT) return false; + + /* Some translucent blocks may still cull other translucent blocks */ + /* e.g. for water/ice, don't need to draw faces of water */ + bType = Blocks.Collide[block]; oType = Blocks.Collide[other]; + return (bType == COLLIDE_SOLID && oType == COLLIDE_SOLID) || bType != COLLIDE_SOLID; +} + +static void Block_CalcCulling(BlockID block, BlockID other) { + Vec3 bMin, bMax, oMin, oMax; + cc_bool occludedX, occludedY, occludedZ, bothLiquid; + int f; + + /* Fast path: Full opaque neighbouring blocks will always have all shared faces hidden */ + if (Blocks.FullOpaque[block] && Blocks.FullOpaque[other]) { + Blocks.Hidden[(block * BLOCK_COUNT) + other] = 0x3F; + return; + } + + /* Some blocks may not cull 'other' block, in which case just skip detailed check */ + /* e.g. sprite blocks, default leaves, will not cull any other blocks */ + if (!Block_MightCull(block, other)) { + Blocks.Hidden[(block * BLOCK_COUNT) + other] = 0; + return; + } + + bMin = Blocks.MinBB[block]; bMax = Blocks.MaxBB[block]; + oMin = Blocks.MinBB[other]; oMax = Blocks.MaxBB[other]; + + /* Extend offsets of liquid down to match rendered position */ + /* This isn't completely correct, but works well enough */ + if (Blocks.IsLiquid[block]) bMax.y -= 1.50f / 16.0f; + if (Blocks.IsLiquid[other]) oMax.y -= 1.50f / 16.0f; + + bothLiquid = Blocks.IsLiquid[block] && Blocks.IsLiquid[other]; + f = 0; /* mark all faces initially 'not hidden' */ + + /* Whether the 'texture region' of a face on block fits inside corresponding region on other block */ + occludedX = (bMin.z >= oMin.z && bMax.z <= oMax.z) && (bMin.y >= oMin.y && bMax.y <= oMax.y); + occludedY = (bMin.x >= oMin.x && bMax.x <= oMax.x) && (bMin.z >= oMin.z && bMax.z <= oMax.z); + occludedZ = (bMin.x >= oMin.x && bMax.x <= oMax.x) && (bMin.y >= oMin.y && bMax.y <= oMax.y); + + f |= occludedX && oMax.x == 1.0f && bMin.x == 0.0f ? (1 << FACE_XMIN) : 0; + f |= occludedX && oMin.x == 0.0f && bMax.x == 1.0f ? (1 << FACE_XMAX) : 0; + f |= occludedZ && oMax.z == 1.0f && bMin.z == 0.0f ? (1 << FACE_ZMIN) : 0; + f |= occludedZ && oMin.z == 0.0f && bMax.z == 1.0f ? (1 << FACE_ZMAX) : 0; + f |= occludedY && (bothLiquid || (oMax.y == 1.0f && bMin.y == 0.0f)) ? (1 << FACE_YMIN) : 0; + f |= occludedY && (bothLiquid || (oMin.y == 0.0f && bMax.y == 1.0f)) ? (1 << FACE_YMAX) : 0; + Blocks.Hidden[(block * BLOCK_COUNT) + other] = f; +} + +/* Updates culling data of all blocks */ +static void Block_UpdateAllCulling(void) { + int block, neighbour; + + for (block = BLOCK_AIR; block < BLOCK_COUNT; block++) { + Block_CalcStretch((BlockID)block); + for (neighbour = BLOCK_AIR; neighbour < BLOCK_COUNT; neighbour++) { + Block_CalcCulling((BlockID)block, (BlockID)neighbour); + } + } +} + +/* Updates culling data just for this block */ +/* (e.g. whether block can be stretched, visibility with other blocks) */ +static void Block_UpdateCulling(BlockID block) { + int neighbour; + Block_CalcStretch(block); + + for (neighbour = BLOCK_AIR; neighbour < BLOCK_COUNT; neighbour++) { + Block_CalcCulling(block, (BlockID)neighbour); + Block_CalcCulling((BlockID)neighbour, block); + } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Block-----------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint32 definedCustomBlocks[BLOCK_COUNT >> 5]; +static char Block_NamesBuffer[STRING_SIZE * BLOCK_COUNT]; +#define Block_NamePtr(i) &Block_NamesBuffer[STRING_SIZE * i] + +cc_bool Block_IsCustomDefined(BlockID block) { + return (definedCustomBlocks[block >> 5] & (1u << (block & 0x1F))) != 0; +} + +/* Sets whether the given block has been changed from default */ +static void Block_SetCustomDefined(BlockID block, cc_bool defined) { + if (defined) { + definedCustomBlocks[block >> 5] |= (1u << (block & 0x1F)); + } else { + definedCustomBlocks[block >> 5] &= ~(1u << (block & 0x1F)); + } +} + +void Block_DefineCustom(BlockID block, cc_bool checkSprite) { + PackedCol black = PackedCol_Make(0, 0, 0, 255); + cc_string name = Block_UNSAFE_GetName(block); + /* necessary if servers redefined core blocks, before extended collide types were added */ + cc_uint8 collide = DefaultSet_MapOldCollide(block, Blocks.Collide[block]); + Blocks.Tinted[block] = Blocks.FogCol[block] != black && String_IndexOf(&name, '#') >= 0; + + Block_SetCollide(block, collide); + Block_SetDrawType(block, Blocks.Draw[block]); + Block_CalcRenderBounds(block); + Block_UpdateCulling(block); + Block_CalcLightOffset(block); + + Inventory_AddDefault(block); + Block_SetCustomDefined(block, true); + Event_RaiseVoid(&BlockEvents.BlockDefChanged); + + if (!checkSprite) return; /* TODO eliminate this */ + /* Update sprite BoundingBox if necessary */ + if (Blocks.Draw[block] == DRAW_SPRITE) Block_RecalculateBB(block); +} + +void Block_UndefineCustom(BlockID block) { + Block_ResetProps(block); + Block_UpdateCulling(block); + + Inventory_Remove(block); + if (block <= BLOCK_MAX_CPE) { Inventory_AddDefault(block); } + + Block_SetCustomDefined(block, false); + Event_RaiseVoid(&BlockEvents.BlockDefChanged); + + /* Update sprite BoundingBox if necessary */ + if (Blocks.Draw[block] == DRAW_SPRITE) Block_RecalculateBB(block); +} + +void Block_ResetProps(BlockID block) { + const struct SimpleBlockDef* def = block <= Game_Version.MaxCoreBlock ? &core_blockDefs[block] : &invalid_blockDef; + const cc_string name = String_FromReadonly(def->name); + + Blocks.BlocksLight[block] = def->blocksLight; + Blocks.Brightness[block] = def->brightness; + Blocks.FogCol[block] = def->fogColor; + Blocks.FogDensity[block] = def->fogDensity / 100.0f; + Block_SetCollide(block, def->collide); + Blocks.DigSounds[block] = def->digSound; + Blocks.StepSounds[block] = def->stepSound; + Blocks.SpeedMultiplier[block] = 1.0f; + Block_SetName(block, &name); + Blocks.Tinted[block] = false; + Blocks.SpriteOffset[block] = 0; + + Blocks.Draw[block] = def->draw; + if (def->draw == DRAW_SPRITE) { + Vec3_Set(Blocks.MinBB[block], 2.50f/16.0f, 0, 2.50f/16.0f); + Vec3_Set(Blocks.MaxBB[block], 13.5f/16.0f, 1, 13.5f/16.0f); + } else { + Vec3_Set(Blocks.MinBB[block], 0, 0, 0); + Vec3_Set(Blocks.MaxBB[block], 1, def->height / 16.0f, 1); + } + + Block_SetDrawType(block, def->draw); + Block_CalcRenderBounds(block); + Block_CalcLightOffset(block); + + Block_Tex(block, FACE_YMAX) = def->topTexture; + Block_Tex(block, FACE_YMIN) = def->bottomTexture; + Block_SetSide(def->sideTexture, block); + + Blocks.ParticleGravity[block] = 5.4f * (def->gravity / 100.0f); +} + +STRING_REF cc_string Block_UNSAFE_GetName(BlockID block) { + return String_FromRaw(Block_NamePtr(block), STRING_SIZE); +} + +void Block_SetName(BlockID block, const cc_string* name) { + String_CopyToRaw(Block_NamePtr(block), STRING_SIZE, name); +} + +int Block_FindID(const cc_string* name) { + cc_string blockName; + int block; + + for (block = BLOCK_AIR; block < BLOCK_COUNT; block++) + { + blockName = Block_UNSAFE_GetName(block); + if (String_CaselessEquals(&blockName, name)) return block; + } + return -1; +} + +int Block_Parse(const cc_string* name) { + int b; + if (Convert_ParseInt(name, &b) && b < BLOCK_COUNT) return b; + return Block_FindID(name); +} + +/* 0b_1000_0000 */ +#define USE_MODERN_BRIGHTNESS_FLAG 1 << 7 +/* 0b_0100_0000 */ +#define USE_LAMP_COLOR 1 << 6 +/* 0b_0000_1111 */ +#define BRIGHTNESS_MASK FANCY_LIGHTING_MAX_LEVEL + +/* Reads network format 0b_US--_LLLL where U = uses fancy brightness, S = uses lamp brightness, and L = brightness */ +/* Into CC's native format 0b_SSSS_BBBB where S = lamp brightness and B = lava brightness */ +cc_uint8 Block_ReadBrightness(cc_uint8 fullBright) { + cc_bool useSun; + /* If the fullBright byte does not use the flag, we should interpret it as either completely dark or casting max block light */ + if ((fullBright & USE_MODERN_BRIGHTNESS_FLAG) == 0) { return fullBright > 0 ? FANCY_LIGHTING_MAX_LEVEL : 0; } + + useSun = fullBright & USE_LAMP_COLOR; + + /* Preserve only the least significant four bits. This gives us our raw brightness level for sun or block light. */ + fullBright &= BRIGHTNESS_MASK; + + /* Sun light is stored in the upper four bits */ + if (useSun) { fullBright <<= FANCY_LIGHTING_LAMP_SHIFT; } + return fullBright; +} + +/* Writes CC's native format 0b_SSSS_BBBB where S = lamp brightness and B = lava brightness */ +/* into network format 0b_US--_LLLL where U = uses fancy brightness, S = uses lamp brightness, and L = brightness */ +cc_uint8 Block_WriteFullBright(cc_uint8 brightness) { + cc_uint8 lavaBrightness, lampBrightness, fullBright; + lavaBrightness = brightness & BRIGHTNESS_MASK; + lampBrightness = brightness >> FANCY_LIGHTING_LAMP_SHIFT; + fullBright = USE_MODERN_BRIGHTNESS_FLAG; + + /* Modern brightness stored in a fullbright value is mutually exclusive between using block and using sun light */ + if (lavaBrightness > 0) { + fullBright |= lavaBrightness; + } else if (lampBrightness > 0) { + fullBright |= USE_LAMP_COLOR; /* Insert flag that tells us this fullbright value should be interpreted as sun brightness */ + fullBright |= lampBrightness; + } else { + return 0; + } + return fullBright; +} + +/*########################################################################################################################* +*-------------------------------------------------------AutoRotate--------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool AutoRotate_Enabled; + +#define AR_GROUP_CORNERS 0 +#define AR_GROUP_VERTICAL 1 +#define AR_GROUP_DIRECTION 2 +#define AR_GROUP_PILLAR 3 + +#define AR_EQ1(x) (dir0 == x && dir1 == '\0') +#define AR_EQ2(x, y) (dir0 == x && dir1 == y) +static int AR_CalcGroup(const cc_string* dir) { + char dir0, dir1; + dir0 = dir->length > 1 ? dir->buffer[1] : '\0'; Char_MakeLower(dir0); + dir1 = dir->length > 2 ? dir->buffer[2] : '\0'; Char_MakeLower(dir1); + + if (AR_EQ2('n','w') || AR_EQ2('n','e') || AR_EQ2('s','w') || AR_EQ2('s','e')) { + return AR_GROUP_CORNERS; + } else if (AR_EQ1('u') || AR_EQ1('d')) { + return AR_GROUP_VERTICAL; + } else if (AR_EQ1('n') || AR_EQ1('w') || AR_EQ1('s') || AR_EQ1('e')) { + return AR_GROUP_DIRECTION; + } else if (AR_EQ2('u','d') || AR_EQ2('w','e') || AR_EQ2('n','s')) { + return AR_GROUP_PILLAR; + } + return -1; +} + +/* replaces a portion of a string, appends otherwise */ +static void AutoRotate_Insert(cc_string* str, int offset, const char* suffix) { + int i = str->length - offset; + + for (; *suffix; suffix++, i++) { + if (i < str->length) { + str->buffer[i] = *suffix; + } else { + String_Append(str, *suffix); + } + } +} +/* finds proper rotated form of a block, based on the given name */ +static int FindRotated(cc_string* name, int offset); + +static int GetRotated(cc_string* name, int offset) { + int rotated = FindRotated(name, offset); + return rotated == -1 ? Block_FindID(name) : rotated; +} + +static int RotateCorner(cc_string* name, int offset) { + float x = Game_SelectedPos.intersect.x - (float)Game_SelectedPos.translatedPos.x; + float z = Game_SelectedPos.intersect.z - (float)Game_SelectedPos.translatedPos.z; + + if (x < 0.5f && z < 0.5f) { + AutoRotate_Insert(name, offset, "-NW"); + } else if (x >= 0.5f && z < 0.5f) { + AutoRotate_Insert(name, offset, "-NE"); + } else if (x < 0.5f && z >= 0.5f) { + AutoRotate_Insert(name, offset, "-SW"); + } else if (x >= 0.5f && z >= 0.5f) { + AutoRotate_Insert(name, offset, "-SE"); + } + return GetRotated(name, offset); +} + +static int RotateVertical(cc_string* name, int offset) { + float y = Game_SelectedPos.intersect.y - (float)Game_SelectedPos.translatedPos.y; + + if (y >= 0.5f) { + AutoRotate_Insert(name, offset, "-U"); + } else { + AutoRotate_Insert(name, offset, "-D"); + } + return GetRotated(name, offset); +} + +static int RotateFence(cc_string* name, int offset) { + /* Fence type blocks */ + float yaw = Math_ClampAngle(Entities.CurPlayer->Base.Yaw); + + if (yaw < 45.0f || (yaw >= 135.0f && yaw < 225.0f) || yaw > 315.0f) { + AutoRotate_Insert(name, offset, "-WE"); + } else { + AutoRotate_Insert(name, offset, "-NS"); + } + return GetRotated(name, offset); +} + +static int RotatePillar(cc_string* name, int offset) { + /* Thin pillar type blocks */ + Face face = Game_SelectedPos.closest; + + if (face == FACE_YMAX || face == FACE_YMIN) { + AutoRotate_Insert(name, offset, "-UD"); + } else if (face == FACE_XMAX || face == FACE_XMIN) { + AutoRotate_Insert(name, offset, "-WE"); + } else if (face == FACE_ZMAX || face == FACE_ZMIN) { + AutoRotate_Insert(name, offset, "-NS"); + } + return GetRotated(name, offset); +} + +static int RotateDirection(cc_string* name, int offset) { + float yaw = Math_ClampAngle(Entities.CurPlayer->Base.Yaw); + + if (yaw >= 45.0f && yaw < 135.0f) { + AutoRotate_Insert(name, offset, "-E"); + } else if (yaw >= 135.0f && yaw < 225.0f) { + AutoRotate_Insert(name, offset, "-S"); + } else if (yaw >= 225.0f && yaw < 315.0f) { + AutoRotate_Insert(name, offset, "-W"); + } else { + AutoRotate_Insert(name, offset, "-N"); + } + return GetRotated(name, offset); +} + +static int FindRotated(cc_string* name, int offset) { + cc_string dir; + int group; + int dirIndex = String_LastIndexOfAt(name, offset, '-'); + if (dirIndex == -1) return -1; /* not a directional block */ + + dir = String_UNSAFE_SubstringAt(name, dirIndex); + dir.length -= offset; + if (dir.length > 3) return -1; + + offset += dir.length; + group = AR_CalcGroup(&dir); + + if (group == AR_GROUP_CORNERS) return RotateCorner(name, offset); + if (group == AR_GROUP_VERTICAL) return RotateVertical(name, offset); + if (group == AR_GROUP_DIRECTION) return RotateDirection(name, offset); + + if (group == AR_GROUP_PILLAR) { + AutoRotate_Insert(name, offset, "-UD"); + if (Block_FindID(name) == -1) { + return RotateFence(name, offset); + } else { + return RotatePillar(name, offset); + } + } + return -1; +} + +BlockID AutoRotate_RotateBlock(BlockID block) { + cc_string str; char strBuffer[STRING_SIZE * 2]; + cc_string name; + int rotated; + + name = Block_UNSAFE_GetName(block); + String_InitArray(str, strBuffer); + String_AppendString(&str, &name); + + /* need to copy since we change characters in name */ + rotated = FindRotated(&str, 0); + return rotated == -1 ? block : (BlockID)rotated; +} + +static void GetAutoRotateTypes(cc_string* name, int* dirTypes) { + int dirIndex, i; + cc_string dir; + dirTypes[0] = -1; + dirTypes[1] = -1; + + for (i = 0; i < 2; i++) { + /* index of rightmost group separated by dashes */ + dirIndex = String_LastIndexOf(name, '-'); + if (dirIndex == -1) return; + + dir = String_UNSAFE_SubstringAt(name, dirIndex); + dirTypes[i] = AR_CalcGroup(&dir); + name->length = dirIndex; + } +} + +cc_bool AutoRotate_BlocksShareGroup(BlockID block, BlockID other) { + cc_string bName, oName; + int bDirTypes[2], oDirTypes[2]; + + bName = Block_UNSAFE_GetName(block); + GetAutoRotateTypes(&bName, bDirTypes); + if (bDirTypes[0] == -1) return false; + + oName = Block_UNSAFE_GetName(other); + GetAutoRotateTypes(&oName, oDirTypes); + if (oDirTypes[0] == -1) return false; + + return bDirTypes[0] == oDirTypes[0] && bDirTypes[1] == oDirTypes[1] + && String_CaselessEquals(&bName, &oName); +} + + +/*########################################################################################################################* +*----------------------------------------------------Blocks component-----------------------------------------------------* +*#########################################################################################################################*/ +static void OnReset(void) { + int i, block; + for (i = 0; i < Array_Elems(definedCustomBlocks); i++) { + definedCustomBlocks[i] = 0; + } + + for (block = BLOCK_AIR; block < BLOCK_COUNT; block++) { + Block_ResetProps((BlockID)block); + } + + Block_UpdateAllCulling(); + Block_RecalculateAllSpriteBB(); + + for (block = BLOCK_AIR; block < BLOCK_COUNT; block++) { + Blocks.CanPlace[block] = true; + Blocks.CanDelete[block] = true; + } +} + +static void OnAtlasChanged(void* obj) { Block_RecalculateAllSpriteBB(); } +static void OnInit(void) { + AutoRotate_Enabled = true; + Event_Register_(&TextureEvents.AtlasChanged, NULL, OnAtlasChanged); + OnReset(); +} + +struct IGameComponent Blocks_Component = { + OnInit, /* Init */ + NULL, /* Free */ + OnReset, /* Reset */ +}; diff --git a/src/Block.h b/src/Block.h new file mode 100644 index 0000000..9a51813 --- /dev/null +++ b/src/Block.h @@ -0,0 +1,154 @@ +#ifndef CC_BLOCK_H +#define CC_BLOCK_H +#include "PackedCol.h" +#include "Vectors.h" +#include "Constants.h" +#include "BlockID.h" +/* Stores properties and data for blocks. + Also performs automatic rotation of directional blocks. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent Blocks_Component; + +enum SoundType { + SOUND_NONE, SOUND_WOOD, SOUND_GRAVEL, SOUND_GRASS, + SOUND_STONE, SOUND_METAL, SOUND_GLASS, SOUND_CLOTH, + SOUND_SAND, SOUND_SNOW, SOUND_COUNT +}; +extern const char* const Sound_Names[SOUND_COUNT]; + +/* Describes how a block is rendered in the world. */ +enum DrawType { + DRAW_OPAQUE, /* Completely covers blocks behind (e.g. dirt). */ + DRAW_TRANSPARENT, /* Blocks behind show (e.g. glass). Pixels either fully visible or invisible. */ + DRAW_TRANSPARENT_THICK, /* Same as Transparent, but all neighbour faces show. (e.g. leaves) */ + DRAW_TRANSLUCENT, /* Blocks behind show (e.g. water). Pixels blend with other blocks behind. */ + DRAW_GAS, /* Does not show (e.g. air). Can still be collided with. */ + DRAW_SPRITE /* Block renders as an X (e.g. sapling). Pixels either fully visible or invisible. */ +}; + +/* Describes the interaction a block has with a player when they collide with it. */ +enum CollideType { + COLLIDE_NONE, /* No interaction when player collides. */ + COLLIDE_LIQUID, /* 'swimming'/'bobbing' interaction when player collides. */ + COLLIDE_SOLID, /* Block completely stops the player when they are moving. */ + COLLIDE_ICE, /* Block is solid and partially slidable on. */ + COLLIDE_SLIPPERY_ICE, /* Block is solid and fully slidable on. */ + COLLIDE_WATER, /* Water style 'swimming'/'bobbing' interaction when player collides. */ + COLLIDE_LAVA, /* Lava style 'swimming'/'bobbing' interaction when player collides. */ + COLLIDE_CLIMB /* Rope/Ladder style climbing interaction when player collides. */ +}; + +CC_VAR extern struct _BlockLists { + /* Whether this block is a liquid. (Like water/lava) */ + cc_bool IsLiquid[BLOCK_COUNT]; + /* Whether this block prevents lights from passing through it. */ + cc_bool BlocksLight[BLOCK_COUNT]; + /* Whether this block is fully bright/light emitting. (Like lava) */ + cc_uint8 Brightness[BLOCK_COUNT]; + /* Fog colour when player is inside this block. */ + /* NOTE: Only applies if fog density is not 0. */ + PackedCol FogCol[BLOCK_COUNT]; + /* How thick fog is when player is inside this block. */ + float FogDensity[BLOCK_COUNT]; + /* Basic collision type of this block. (gas, liquid, or solid) */ + cc_uint8 Collide[BLOCK_COUNT]; + /* Extended collision type of this block, usually same as basic. */ + /* NOTE: Not always the case. (e.g. ice, water, lava, rope differ) */ + cc_uint8 ExtendedCollide[BLOCK_COUNT]; + /* Speed multiplier when player is touching this block. */ + /* Can be < 1 to slow player down, or > 1 to speed up. */ + float SpeedMultiplier[BLOCK_COUNT]; + /* Bit flags of which faces of this block uses light colour from neighbouring blocks. */ + /* e.g. a block with Min.x of 0.0 uses light colour at X-1,Y,Z for XMIN face. */ + /* e.g. a block with Min.x of 0.1 uses light colour at X,Y,Z for XMIN face. */ + cc_uint8 LightOffset[BLOCK_COUNT]; + /* Draw method used when rendering this block. See DrawType enum. */ + cc_uint8 Draw[BLOCK_COUNT]; + /* Sound played when the player manually destroys this block. See SoundType enum. */ + cc_uint8 DigSounds[BLOCK_COUNT]; + /* Sound played when the player walks on this block. See SoundType enum. */ + cc_uint8 StepSounds[BLOCK_COUNT]; + /* Whether fog colour is used to apply a tint effect to this block. */ + cc_bool Tinted[BLOCK_COUNT]; + /* Whether this block has an opaque draw type, min of (0,0,0), and max of (1,1,1) */ + cc_bool FullOpaque[BLOCK_COUNT]; + /* Offset/variation mode of this block. (only when drawn as a sprite) */ + /* Some modes slightly randomly offset blocks to produce nicer looking clumps. */ + cc_uint8 SpriteOffset[BLOCK_COUNT]; + + /* Coordinates of min corner of this block for collisions. */ + Vec3 MinBB[BLOCK_COUNT]; + /* Coordinates of max corner of this block for collisions. */ + Vec3 MaxBB[BLOCK_COUNT]; + /* Coordinates of min corner of this block for rendering. */ + /* e.g. ice is very slightly offset horizontally. */ + Vec3 RenderMinBB[BLOCK_COUNT]; + /* Coordinates of max corner of this block for rendering. */ + /* e.g. ice is very slightly offset horizontally. */ + Vec3 RenderMaxBB[BLOCK_COUNT]; + + /* Texture ids of each face of blocks. */ + TextureLoc Textures[BLOCK_COUNT * FACE_COUNT]; + /* Whether this block is allowed to be placed. */ + cc_bool CanPlace[BLOCK_COUNT]; + /* Whether this block is allowed to be deleted. */ + cc_bool CanDelete[BLOCK_COUNT]; + + /* Bit flags of faces hidden of two neighbouring blocks. */ + cc_uint8 Hidden[BLOCK_COUNT * BLOCK_COUNT]; + /* Bit flags of which faces of this block can stretch with greedy meshing. */ + cc_uint8 CanStretch[BLOCK_COUNT]; + /* Gravity of particles spawned when this block is broken */ + float ParticleGravity[BLOCK_COUNT]; +} Blocks; + +#define Block_Tint(col, block)\ +if (Blocks.Tinted[block]) col = PackedCol_Tint(col, Blocks.FogCol[block]); + +/* Most blocks which 'stop' light actually stop the light starting at block below */ +/* except for e.g. upside down slabs which 'stop' the light at same block level */ +/* The difference can be seen by placing a lower and upper slab block on a wall, */ +/* and comparing whether the block directly behind them is in shadow or not */ +#define LIGHT_FLAG_SHADES_FROM_BELOW 6 +cc_uint8 Block_ReadBrightness(cc_uint8 fullBright); +cc_uint8 Block_WriteFullBright(cc_uint8 brightness); + +/* Returns whether the given block has been changed from default */ +cc_bool Block_IsCustomDefined(BlockID block); +/* Updates state and raises event after the given block has been defined */ +void Block_DefineCustom(BlockID block, cc_bool checkSprite); +/* Resets the given block to default */ +void Block_UndefineCustom(BlockID block); +/* Resets all the properties of the given block to default */ +void Block_ResetProps(BlockID block); + +/* Gets the name of the given block */ +/* NOTE: Name points directly within underlying buffer, you MUST NOT persist this string */ +CC_API STRING_REF cc_string Block_UNSAFE_GetName( BlockID block); +typedef STRING_REF cc_string (*FP_Block_UNSAFE_GetName)(BlockID block); +/* Sets the name of the given block. */ +void Block_SetName(BlockID block, const cc_string* name); +/* Finds the ID of the block whose name caselessly matches given name */ +CC_API int Block_FindID(const cc_string* name); +/* Attempts to parse given name as a numerical block ID */ +/* Falls back to Block_FindID if this fails */ +CC_API int Block_Parse(const cc_string* name); + +/* Sets the textures of the side faces of the given block */ +void Block_SetSide(TextureLoc texLoc, BlockID blockId); +/* The texture for the given face of the given block */ +#define Block_Tex(block, face) Blocks.Textures[(block) * FACE_COUNT + (face)] + +/* Whether the given face of this block is occluded/hidden */ +#define Block_IsFaceHidden(block, other, face) (Blocks.Hidden[((block) * BLOCK_COUNT) + (other)] & (1 << (face))) + +/* Whether blocks can be automatically rotated */ +extern cc_bool AutoRotate_Enabled; +/* Attempts to find the rotated block based on the user's orientation and offset on selected block */ +/* If no rotated block is found, returns given block */ +BlockID AutoRotate_RotateBlock(BlockID block); +/* Returns non 0 if both blocks belong to the same autorotate group */ +cc_bool AutoRotate_BlocksShareGroup(BlockID block, BlockID blockOther); +#endif diff --git a/src/BlockID.h b/src/BlockID.h new file mode 100644 index 0000000..882c581 --- /dev/null +++ b/src/BlockID.h @@ -0,0 +1,91 @@ +#ifndef CC_BLOCKID_H +#define CC_BLOCKID_H +#include "Core.h" /* TODO: Remove this include when we move to external defines */ +/* List of all core/standard block IDs + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +enum BLOCKID { + /* Classic blocks */ + BLOCK_AIR = 0, + BLOCK_STONE = 1, + BLOCK_GRASS = 2, + BLOCK_DIRT = 3, + BLOCK_COBBLE = 4, + BLOCK_WOOD = 5, + BLOCK_SAPLING = 6, + BLOCK_BEDROCK = 7, + BLOCK_WATER = 8, + BLOCK_STILL_WATER = 9, + BLOCK_LAVA = 10, + BLOCK_STILL_LAVA = 11, + BLOCK_SAND = 12, + BLOCK_GRAVEL = 13, + BLOCK_GOLD_ORE = 14, + BLOCK_IRON_ORE = 15, + BLOCK_COAL_ORE = 16, + BLOCK_LOG = 17, + BLOCK_LEAVES = 18, + BLOCK_SPONGE = 19, + BLOCK_GLASS = 20, + BLOCK_RED = 21, + BLOCK_ORANGE = 22, + BLOCK_YELLOW = 23, + BLOCK_LIME = 24, + BLOCK_GREEN = 25, + BLOCK_TEAL = 26, + BLOCK_AQUA = 27, + BLOCK_CYAN = 28, + BLOCK_BLUE = 29, + BLOCK_INDIGO = 30, + BLOCK_VIOLET = 31, + BLOCK_MAGENTA = 32, + BLOCK_PINK = 33, + BLOCK_BLACK = 34, + BLOCK_GRAY = 35, + BLOCK_WHITE = 36, + BLOCK_DANDELION = 37, + BLOCK_ROSE = 38, + BLOCK_BROWN_SHROOM = 39, + BLOCK_RED_SHROOM = 40, + BLOCK_GOLD = 41, + BLOCK_IRON = 42, + BLOCK_DOUBLE_SLAB = 43, + BLOCK_SLAB = 44, + BLOCK_BRICK = 45, + BLOCK_TNT = 46, + BLOCK_BOOKSHELF = 47, + BLOCK_MOSSY_ROCKS = 48, + BLOCK_OBSIDIAN = 49, + + /* CPE blocks */ + BLOCK_COBBLE_SLAB = 50, + BLOCK_ROPE = 51, + BLOCK_SANDSTONE = 52, + BLOCK_SNOW = 53, + BLOCK_FIRE = 54, + BLOCK_LIGHT_PINK = 55, + BLOCK_FOREST_GREEN = 56, + BLOCK_BROWN = 57, + BLOCK_DEEP_BLUE = 58, + BLOCK_TURQUOISE = 59, + BLOCK_ICE = 60, + BLOCK_CERAMIC_TILE = 61, + BLOCK_MAGMA = 62, + BLOCK_PILLAR = 63, + BLOCK_CRATE = 64, + BLOCK_STONE_BRICK = 65, + + /* Max block ID used in original classic */ + BLOCK_MAX_ORIGINAL = BLOCK_OBSIDIAN, + /* Max block ID used in original classic plus CPE blocks. */ + BLOCK_MAX_CPE = BLOCK_STONE_BRICK, + +#ifdef EXTENDED_BLOCKS + BLOCK_MAX_DEFINED = 0x2FF, +#else + BLOCK_MAX_DEFINED = 0xFF, +#endif + BLOCK_COUNT = (BLOCK_MAX_DEFINED + 1) +}; +#endif diff --git a/src/BlockPhysics.c b/src/BlockPhysics.c new file mode 100644 index 0000000..0cb7a2c --- /dev/null +++ b/src/BlockPhysics.c @@ -0,0 +1,573 @@ +#include "BlockPhysics.h" +#include "World.h" +#include "Constants.h" +#include "Funcs.h" +#include "Event.h" +#include "ExtMath.h" +#include "Block.h" +#include "Lighting.h" +#include "Options.h" +#include "Generator.h" +#include "Platform.h" +#include "Game.h" +#include "Logger.h" +#include "Vectors.h" +#include "Chat.h" + +/* Data for a resizable queue, used for liquid physic tick entries. */ +struct TickQueue { + cc_uint32* entries; /* Buffer holding the items in the tick queue */ + int capacity; /* Max number of elements in the buffer */ + int mask; /* capacity - 1, as capacity is always a power of two */ + int count; /* Number of used elements */ + int head; /* Head index into the buffer */ + int tail; /* Tail index into the buffer */ +}; + +static void TickQueue_Init(struct TickQueue* queue) { + queue->entries = NULL; + queue->capacity = 0; + queue->mask = 0; + queue->count = 0; + queue->head = 0; + queue->tail = 0; +} + +static void TickQueue_Clear(struct TickQueue* queue) { + if (!queue->entries) return; + Mem_Free(queue->entries); + TickQueue_Init(queue); +} + +static void TickQueue_Resize(struct TickQueue* queue) { + cc_uint32* entries; + int i, idx, capacity; + + if (queue->capacity >= (Int32_MaxValue / 4)) { + Chat_AddRaw("&cToo many physics entries, clearing"); + TickQueue_Clear(queue); + } + + capacity = queue->capacity * 2; + if (capacity < 32) capacity = 32; + entries = (cc_uint32*)Mem_Alloc(capacity, 4, "physics tick queue"); + + /* Elements must be readjusted to avoid index wrapping issues */ + /* https://stackoverflow.com/questions/55343683/resizing-of-the-circular-queue-using-dynamic-array */ + for (i = 0; i < queue->count; i++) { + idx = (queue->head + i) & queue->mask; + entries[i] = queue->entries[idx]; + } + Mem_Free(queue->entries); + + queue->entries = entries; + queue->capacity = capacity; + queue->mask = capacity - 1; /* capacity is power of two */ + queue->head = 0; + queue->tail = queue->count; +} + +/* Appends an entry to the end of the queue, resizing if necessary. */ +static void TickQueue_Enqueue(struct TickQueue* queue, cc_uint32 item) { + if (queue->count == queue->capacity) + TickQueue_Resize(queue); + + queue->entries[queue->tail] = item; + queue->tail = (queue->tail + 1) & queue->mask; + queue->count++; +} + +/* Retrieves the entry from the front of the queue. */ +static cc_uint32 TickQueue_Dequeue(struct TickQueue* queue) { + cc_uint32 result = queue->entries[queue->head]; + queue->head = (queue->head + 1) & queue->mask; + queue->count--; + return result; +} + + +struct Physics_ Physics; +static RNGState physics_rnd; +static int physics_tickCount; +static int physics_maxWaterX, physics_maxWaterY, physics_maxWaterZ; +static struct TickQueue lavaQ, waterQ; + +#define PHYSICS_DELAY_MASK 0xF8000000UL +#define PHYSICS_POS_MASK 0x07FFFFFFUL +#define PHYSICS_DELAY_SHIFT 27 +#define PHYSICS_ONE_DELAY (1U << PHYSICS_DELAY_SHIFT) +#define PHYSICS_LAVA_DELAY (30U << PHYSICS_DELAY_SHIFT) +#define PHYSICS_WATER_DELAY (5U << PHYSICS_DELAY_SHIFT) + +static void Physics_OnNewMapLoaded(void* obj) { + TickQueue_Clear(&lavaQ); + TickQueue_Clear(&waterQ); + + physics_maxWaterX = World.MaxX - 2; + physics_maxWaterY = World.MaxY - 2; + physics_maxWaterZ = World.MaxZ - 2; + + Tree_Blocks = World.Blocks; + Random_SeedFromCurrentTime(&physics_rnd); + Tree_Rnd = &physics_rnd; +} + +void Physics_SetEnabled(cc_bool enabled) { + Physics.Enabled = enabled; + Physics_OnNewMapLoaded(NULL); +} + +static void Physics_Activate(int index) { + BlockID block = World.Blocks[index]; + PhysicsHandler activate = Physics.OnActivate[block]; + if (activate) activate(index, block); +} + +static void Physics_ActivateNeighbours(int x, int y, int z, int index) { + if (x > 0) Physics_Activate(index - 1); + if (x < World.MaxX) Physics_Activate(index + 1); + if (z > 0) Physics_Activate(index - World.Width); + if (z < World.MaxZ) Physics_Activate(index + World.Width); + if (y > 0) Physics_Activate(index - World.OneY); + if (y < World.MaxY) Physics_Activate(index + World.OneY); +} + +static cc_bool Physics_IsEdgeWater(int x, int y, int z) { + return + (Env.EdgeBlock == BLOCK_WATER || Env.EdgeBlock == BLOCK_STILL_WATER) + && (y >= Env_SidesHeight && y < Env.EdgeHeight) + && (x == 0 || z == 0 || x == World.MaxX || z == World.MaxZ); +} + + +void Physics_OnBlockChanged(int x, int y, int z, BlockID old, BlockID now) { + PhysicsHandler handler; + int index; + if (!Physics.Enabled) return; + + if (now == BLOCK_AIR && Physics_IsEdgeWater(x, y, z)) { + now = BLOCK_STILL_WATER; + Game_UpdateBlock(x, y, z, BLOCK_STILL_WATER); + } + index = World_Pack(x, y, z); + + /* User can place/delete blocks over ID 256 */ + if (now == BLOCK_AIR) { + handler = Physics.OnDelete[(BlockRaw)old]; + if (handler) handler(index, old); + } else { + handler = Physics.OnPlace[(BlockRaw)now]; + if (handler) handler(index, now); + } + Physics_ActivateNeighbours(x, y, z, index); +} + +static void Physics_TickRandomBlocks(void) { + int lo, hi, index; + BlockID block; + PhysicsHandler tick; + int x, y, z, x2, y2, z2; + + for (y = 0; y < World.Height; y += CHUNK_SIZE) { + y2 = min(y + CHUNK_MAX, World.MaxY); + for (z = 0; z < World.Length; z += CHUNK_SIZE) { + z2 = min(z + CHUNK_MAX, World.MaxZ); + for (x = 0; x < World.Width; x += CHUNK_SIZE) { + x2 = min(x + CHUNK_MAX, World.MaxX); + + /* Inlined 3 random ticks for this chunk */ + lo = World_Pack( x, y, z); + hi = World_Pack(x2, y2, z2); + + index = Random_Range(&physics_rnd, lo, hi); + block = World.Blocks[index]; + tick = Physics.OnRandomTick[block]; + if (tick) tick(index, block); + + index = Random_Range(&physics_rnd, lo, hi); + block = World.Blocks[index]; + tick = Physics.OnRandomTick[block]; + if (tick) tick(index, block); + + index = Random_Range(&physics_rnd, lo, hi); + block = World.Blocks[index]; + tick = Physics.OnRandomTick[block]; + if (tick) tick(index, block); + } + } + } +} + + +static void Physics_DoFalling(int index, BlockID block) { + int found = -1, start = index; + BlockID other; + int x, y, z; + + /* Find lowest block can fall into */ + while (index >= World.OneY) { + index -= World.OneY; + other = World.Blocks[index]; + + if (other == BLOCK_AIR || (other >= BLOCK_WATER && other <= BLOCK_STILL_LAVA)) + found = index; + else + break; + } + + if (found == -1) return; + World_Unpack(found, x, y, z); + Game_UpdateBlock(x, y, z, block); + + World_Unpack(start, x, y, z); + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Physics_ActivateNeighbours(x, y, z, start); +} + +static cc_bool Physics_CheckItem(struct TickQueue* queue, int* posIndex) { + cc_uint32 item = TickQueue_Dequeue(queue); + *posIndex = (int)(item & PHYSICS_POS_MASK); + + if (item >= PHYSICS_ONE_DELAY) { + item -= PHYSICS_ONE_DELAY; + TickQueue_Enqueue(queue, item); + return false; + } + return true; +} + + +static void Physics_HandleSapling(int index, BlockID block) { + IVec3 coords[TREE_MAX_COUNT]; + BlockRaw blocks[TREE_MAX_COUNT]; + int i, count, height; + + BlockID below; + int x, y, z; + World_Unpack(index, x, y, z); + + below = BLOCK_AIR; + if (y > 0) below = World.Blocks[index - World.OneY]; + if (below != BLOCK_GRASS) return; + + height = 5 + Random_Next(&physics_rnd, 3); + Game_UpdateBlock(x, y, z, BLOCK_AIR); + + if (TreeGen_CanGrow(x, y, z, height)) { + count = TreeGen_Grow(x, y, z, height, coords, blocks); + + for (i = 0; i < count; i++) { + Game_UpdateBlock(coords[i].x, coords[i].y, coords[i].z, blocks[i]); + } + } else { + Game_UpdateBlock(x, y, z, BLOCK_SAPLING); + } +} + +static void Physics_HandleDirt(int index, BlockID block) { + int x, y, z; + World_Unpack(index, x, y, z); + + if (Lighting.IsLit(x, y, z)) { + Game_UpdateBlock(x, y, z, BLOCK_GRASS); + } +} + +static void Physics_HandleGrass(int index, BlockID block) { + int x, y, z; + World_Unpack(index, x, y, z); + + if (!Lighting.IsLit(x, y, z)) { + Game_UpdateBlock(x, y, z, BLOCK_DIRT); + } +} + +static void Physics_HandleFlower(int index, BlockID block) { + BlockID below; + int x, y, z; + World_Unpack(index, x, y, z); + + if (!Lighting.IsLit(x, y, z)) { + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Physics_ActivateNeighbours(x, y, z, index); + return; + } + + below = BLOCK_DIRT; + if (y > 0) below = World.Blocks[index - World.OneY]; + if (!(below == BLOCK_DIRT || below == BLOCK_GRASS)) { + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Physics_ActivateNeighbours(x, y, z, index); + } +} + +static void Physics_HandleMushroom(int index, BlockID block) { + BlockID below; + int x, y, z; + World_Unpack(index, x, y, z); + + if (Lighting.IsLit(x, y, z)) { + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Physics_ActivateNeighbours(x, y, z, index); + return; + } + + below = BLOCK_STONE; + if (y > 0) below = World.Blocks[index - World.OneY]; + if (!(below == BLOCK_STONE || below == BLOCK_COBBLE)) { + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Physics_ActivateNeighbours(x, y, z, index); + } +} + + +static void Physics_PlaceLava(int index, BlockID block) { + TickQueue_Enqueue(&lavaQ, PHYSICS_LAVA_DELAY | index); +} + +static void Physics_PropagateLava(int posIndex, int x, int y, int z) { + BlockID block = World.Blocks[posIndex]; + + if (block >= BLOCK_WATER && block <= BLOCK_STILL_LAVA) { + /* Lava spreading into water turns the water solid */ + if (block == BLOCK_WATER || block == BLOCK_STILL_WATER) { + Game_UpdateBlock(x, y, z, BLOCK_STONE); + } + } else if (Blocks.Collide[block] == COLLIDE_NONE) { + TickQueue_Enqueue(&lavaQ, PHYSICS_LAVA_DELAY | posIndex); + Game_UpdateBlock(x, y, z, BLOCK_LAVA); + } +} + +static void Physics_ActivateLava(int index, BlockID block) { + int x, y, z; + World_Unpack(index, x, y, z); + + if (x > 0) Physics_PropagateLava(index - 1, x - 1, y, z); + if (x < World.MaxX) Physics_PropagateLava(index + 1, x + 1, y, z); + if (z > 0) Physics_PropagateLava(index - World.Width, x, y, z - 1); + if (z < World.MaxZ) Physics_PropagateLava(index + World.Width, x, y, z + 1); + if (y > 0) Physics_PropagateLava(index - World.OneY, x, y - 1, z); +} + +static void Physics_TickLava(void) { + int i, count = lavaQ.count; + for (i = 0; i < count; i++) { + int index; + if (Physics_CheckItem(&lavaQ, &index)) { + BlockID block = World.Blocks[index]; + if (!(block == BLOCK_LAVA || block == BLOCK_STILL_LAVA)) continue; + Physics_ActivateLava(index, block); + } + } +} + + +static void Physics_PlaceWater(int index, BlockID block) { + TickQueue_Enqueue(&waterQ, PHYSICS_WATER_DELAY | index); +} + +static void Physics_PropagateWater(int posIndex, int x, int y, int z) { + BlockID block = World.Blocks[posIndex]; + int xx, yy, zz; + + if (block >= BLOCK_WATER && block <= BLOCK_STILL_LAVA) { + /* Water spreading into lava turns the lava solid */ + if (block == BLOCK_LAVA || block == BLOCK_STILL_LAVA) { + Game_UpdateBlock(x, y, z, BLOCK_STONE); + } + } else if (Blocks.Collide[block] == COLLIDE_NONE) { + /* Sponge check */ + for (yy = (y < 2 ? 0 : y - 2); yy <= (y > physics_maxWaterY ? World.MaxY : y + 2); yy++) { + for (zz = (z < 2 ? 0 : z - 2); zz <= (z > physics_maxWaterZ ? World.MaxZ : z + 2); zz++) { + for (xx = (x < 2 ? 0 : x - 2); xx <= (x > physics_maxWaterX ? World.MaxX : x + 2); xx++) { + block = World_GetBlock(xx, yy, zz); + if (block == BLOCK_SPONGE) return; + } + } + } + + TickQueue_Enqueue(&waterQ, PHYSICS_WATER_DELAY | posIndex); + Game_UpdateBlock(x, y, z, BLOCK_WATER); + } +} + +static void Physics_ActivateWater(int index, BlockID block) { + int x, y, z; + World_Unpack(index, x, y, z); + + if (x > 0) Physics_PropagateWater(index - 1, x - 1, y, z); + if (x < World.MaxX) Physics_PropagateWater(index + 1, x + 1, y, z); + if (z > 0) Physics_PropagateWater(index - World.Width, x, y, z - 1); + if (z < World.MaxZ) Physics_PropagateWater(index + World.Width, x, y, z + 1); + if (y > 0) Physics_PropagateWater(index - World.OneY, x, y - 1, z); +} + +static void Physics_TickWater(void) { + int i, count = waterQ.count; + for (i = 0; i < count; i++) { + int index; + if (Physics_CheckItem(&waterQ, &index)) { + BlockID block = World.Blocks[index]; + if (!(block == BLOCK_WATER || block == BLOCK_STILL_WATER)) continue; + Physics_ActivateWater(index, block); + } + } +} + + +static void Physics_PlaceSponge(int index, BlockID block) { + int x, y, z, xx, yy, zz; + World_Unpack(index, x, y, z); + + for (yy = y - 2; yy <= y + 2; yy++) { + for (zz = z - 2; zz <= z + 2; zz++) { + for (xx = x - 2; xx <= x + 2; xx++) { + if (!World_Contains(xx, yy, zz)) continue; + + block = World_GetBlock(xx, yy, zz); + if (block == BLOCK_WATER || block == BLOCK_STILL_WATER) { + Game_UpdateBlock(xx, yy, zz, BLOCK_AIR); + } + } + } + } +} + +static void Physics_DeleteSponge(int index, BlockID block) { + int x, y, z, xx, yy, zz; + World_Unpack(index, x, y, z); + + for (yy = y - 3; yy <= y + 3; yy++) { + for (zz = z - 3; zz <= z + 3; zz++) { + for (xx = x - 3; xx <= x + 3; xx++) { + if (Math_AbsI(yy - y) == 3 || Math_AbsI(zz - z) == 3 || Math_AbsI(xx - x) == 3) { + if (!World_Contains(xx, yy, zz)) continue; + + index = World_Pack(xx, yy, zz); + block = World.Blocks[index]; + if (block == BLOCK_WATER || block == BLOCK_STILL_WATER) { + TickQueue_Enqueue(&waterQ, index | PHYSICS_ONE_DELAY); + } + } + } + } + } +} + + +static void Physics_HandleSlab(int index, BlockID block) { + int x, y, z; + World_Unpack(index, x, y, z); + if (index < World.OneY) return; + + if (World.Blocks[index - World.OneY] != BLOCK_SLAB) return; + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Game_UpdateBlock(x, y - 1, z, BLOCK_DOUBLE_SLAB); +} + +static void Physics_HandleCobblestoneSlab(int index, BlockID block) { + int x, y, z; + World_Unpack(index, x, y, z); + if (index < World.OneY) return; + + if (World.Blocks[index - World.OneY] != BLOCK_COBBLE_SLAB) return; + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Game_UpdateBlock(x, y - 1, z, BLOCK_COBBLE); +} + + +/* TODO: should this be moved into a precomputed lookup table, instead of calculating every time? */ +/* performance difference probably isn't enough to really matter */ +static cc_bool BlocksTNT(BlockID b) { + /* NOTE: A bit hacky, but works well enough */ + return (b >= BLOCK_WATER && b <= BLOCK_STILL_LAVA) || + (Blocks.ExtendedCollide[b] == COLLIDE_SOLID && (Blocks.DigSounds[b] == SOUND_METAL || Blocks.DigSounds[b] == SOUND_STONE)); +} + +#define TNT_POWER 4 +#define TNT_POWER_SQUARED (TNT_POWER * TNT_POWER) +static void Physics_HandleTnt(int index, BlockID block) { + int x, y, z; + int dx, dy, dz, xx, yy, zz; + + World_Unpack(index, x, y, z); + Game_UpdateBlock(x, y, z, BLOCK_AIR); + Physics_ActivateNeighbours(x, y, z, index); + + for (dy = -TNT_POWER; dy <= TNT_POWER; dy++) { + for (dz = -TNT_POWER; dz <= TNT_POWER; dz++) { + for (dx = -TNT_POWER; dx <= TNT_POWER; dx++) { + if (dx * dx + dy * dy + dz * dz > TNT_POWER_SQUARED) continue; + + xx = x + dx; yy = y + dy; zz = z + dz; + if (!World_Contains(xx, yy, zz)) continue; + index = World_Pack(xx, yy, zz); + + block = World.Blocks[index]; + if (BlocksTNT(block)) continue; + + Game_UpdateBlock(xx, yy, zz, BLOCK_AIR); + Physics_ActivateNeighbours(xx, yy, zz, index); + } + } + } +} + +void Physics_Init(void) { + Event_Register_(&WorldEvents.MapLoaded, NULL, Physics_OnNewMapLoaded); + Physics.Enabled = Options_GetBool(OPT_BLOCK_PHYSICS, true); + TickQueue_Init(&lavaQ); + TickQueue_Init(&waterQ); + + Physics.OnPlace[BLOCK_SAND] = Physics_DoFalling; + Physics.OnPlace[BLOCK_GRAVEL] = Physics_DoFalling; + Physics.OnActivate[BLOCK_SAND] = Physics_DoFalling; + Physics.OnActivate[BLOCK_GRAVEL] = Physics_DoFalling; + Physics.OnRandomTick[BLOCK_SAND] = Physics_DoFalling; + Physics.OnRandomTick[BLOCK_GRAVEL] = Physics_DoFalling; + + Physics.OnRandomTick[BLOCK_SAPLING] = Physics_HandleSapling; + Physics.OnRandomTick[BLOCK_DIRT] = Physics_HandleDirt; + Physics.OnRandomTick[BLOCK_GRASS] = Physics_HandleGrass; + + Physics.OnRandomTick[BLOCK_DANDELION] = Physics_HandleFlower; + Physics.OnRandomTick[BLOCK_ROSE] = Physics_HandleFlower; + Physics.OnRandomTick[BLOCK_RED_SHROOM] = Physics_HandleMushroom; + Physics.OnRandomTick[BLOCK_BROWN_SHROOM] = Physics_HandleMushroom; + + Physics.OnPlace[BLOCK_LAVA] = Physics_PlaceLava; + Physics.OnPlace[BLOCK_WATER] = Physics_PlaceWater; + Physics.OnPlace[BLOCK_SPONGE] = Physics_PlaceSponge; + Physics.OnDelete[BLOCK_SPONGE] = Physics_DeleteSponge; + + Physics.OnActivate[BLOCK_WATER] = Physics.OnPlace[BLOCK_WATER]; + Physics.OnActivate[BLOCK_STILL_WATER] = Physics.OnPlace[BLOCK_WATER]; + Physics.OnActivate[BLOCK_LAVA] = Physics.OnPlace[BLOCK_LAVA]; + Physics.OnActivate[BLOCK_STILL_LAVA] = Physics.OnPlace[BLOCK_LAVA]; + + Physics.OnRandomTick[BLOCK_WATER] = Physics_ActivateWater; + Physics.OnRandomTick[BLOCK_STILL_WATER] = Physics_ActivateWater; + Physics.OnRandomTick[BLOCK_LAVA] = Physics_ActivateLava; + Physics.OnRandomTick[BLOCK_STILL_LAVA] = Physics_ActivateLava; + + Physics.OnPlace[BLOCK_SLAB] = Physics_HandleSlab; + if (Game_ClassicMode) return; + Physics.OnPlace[BLOCK_COBBLE_SLAB] = Physics_HandleCobblestoneSlab; + Physics.OnPlace[BLOCK_TNT] = Physics_HandleTnt; +} + +void Physics_Free(void) { + Event_Unregister_(&WorldEvents.MapLoaded, NULL, Physics_OnNewMapLoaded); +} + +void Physics_Tick(void) { + if (!Physics.Enabled || !World.Blocks) return; + + /*if ((tickCount % 5) == 0) {*/ + Physics_TickLava(); + Physics_TickWater(); + /*}*/ + physics_tickCount++; + Physics_TickRandomBlocks(); +} diff --git a/src/BlockPhysics.h b/src/BlockPhysics.h new file mode 100644 index 0000000..9bd8126 --- /dev/null +++ b/src/BlockPhysics.h @@ -0,0 +1,29 @@ +#ifndef CC_BLOCKPHYSICS_H +#define CC_BLOCKPHYSICS_H +#include "Core.h" +/* Implements simple block physics. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +typedef void (*PhysicsHandler)(int index, BlockID block); + +CC_VAR extern struct Physics_ { + /* Whether block physics are enabled at all. */ + cc_bool Enabled; + /* Called when block is activated by a neighbouring block change. */ + /* e.g. trigger sand falling, water flooding */ + PhysicsHandler OnActivate[256]; + /* Called when this block is randomly activated. */ + /* e.g. grass eventually fading to dirt in darkness */ + PhysicsHandler OnRandomTick[256]; + /* Called when user manually places a block. */ + PhysicsHandler OnPlace[256]; + /* Called when user manually deletes a block. */ + PhysicsHandler OnDelete[256]; +} Physics; + +void Physics_SetEnabled(cc_bool enabled); +void Physics_OnBlockChanged(int x, int y, int z, BlockID old, BlockID now); +void Physics_Init(void); +void Physics_Free(void); +void Physics_Tick(void); +#endif diff --git a/src/Builder.c b/src/Builder.c new file mode 100644 index 0000000..654a2b7 --- /dev/null +++ b/src/Builder.c @@ -0,0 +1,1662 @@ +#include "Builder.h" +#include "Constants.h" +#include "World.h" +#include "Funcs.h" +#include "Lighting.h" +#include "Platform.h" +#include "MapRenderer.h" +#include "Graphics.h" +#include "Drawer.h" +#include "ExtMath.h" +#include "Block.h" +#include "PackedCol.h" +#include "TexturePack.h" +#include "Game.h" +#include "Options.h" + +int Builder_SidesLevel, Builder_EdgeLevel; +/* Packs an index into the 16x16x16 count array. Coordinates range from 0 to 15. */ +#define Builder_PackCount(xx, yy, zz) ((((yy) << 8) | ((zz) << 4) | (xx)) * FACE_COUNT) +/* Packs an index into the 18x18x18 chunk array. Coordinates range from -1 to 16. */ +#define Builder_PackChunk(xx, yy, zz) (((yy) + 1) * EXTCHUNK_SIZE_2 + ((zz) + 1) * EXTCHUNK_SIZE + ((xx) + 1)) + +static BlockID* Builder_Chunk; +static cc_uint8* Builder_Counts; +static int* Builder_BitFlags; +static int Builder_X, Builder_Y, Builder_Z; +static BlockID Builder_Block; +static int Builder_ChunkIndex; +static cc_bool Builder_FullBright; +static int Builder_ChunkEndX, Builder_ChunkEndZ; +static int Builder_Offsets[FACE_COUNT] = { -1,1, -EXTCHUNK_SIZE,EXTCHUNK_SIZE, -EXTCHUNK_SIZE_2,EXTCHUNK_SIZE_2 }; + +static int (*Builder_StretchXLiquid)(int countIndex, int x, int y, int z, int chunkIndex, BlockID block); +static int (*Builder_StretchX)(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face); +static int (*Builder_StretchZ)(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face); +static void (*Builder_RenderBlock)(int countsIndex, int x, int y, int z); +static void (*Builder_PrePrepareChunk)(void); +static void (*Builder_PostPrepareChunk)(void); + +/* Contains state for vertices for a portion of a chunk mesh (vertices that are in a 1D atlas) */ +struct Builder1DPart { + /* Union to save on memory, since chunk building is divided into counting then building phases */ + union FaceData { + struct VertexTextured* vertices[FACE_COUNT]; + int count[FACE_COUNT]; + } faces; + int sCount, sOffset; +}; + +/* Part builder data, for both normal and translucent parts. +The first ATLAS1D_MAX_ATLASES parts are for normal parts, remainder are for translucent parts. */ +static struct Builder1DPart Builder_Parts[ATLAS1D_MAX_ATLASES * 2]; +static struct VertexTextured* Builder_Vertices; + +static int Builder1DPart_VerticesCount(struct Builder1DPart* part) { + int i, count = part->sCount; + for (i = 0; i < FACE_COUNT; i++) { count += part->faces.count[i]; } + return count; +} + +static int Builder1DPart_CalcOffsets(struct Builder1DPart* part, int offset) { + int i, counts[FACE_COUNT]; + part->sOffset = offset; + + /* Have to copy first due to count and vertices fields being a union */ + for (i = 0; i < FACE_COUNT; i++) + { + counts[i] = part->faces.count[i]; + } + + offset += part->sCount; + for (i = 0; i < FACE_COUNT; i++) + { + part->faces.vertices[i] = &Builder_Vertices[offset]; + offset += counts[i]; + } + return offset; +} + +static int Builder_TotalVerticesCount(void) { + int i, count = 0; + for (i = 0; i < ATLAS1D_MAX_ATLASES * 2; i++) { + count += Builder1DPart_VerticesCount(&Builder_Parts[i]); + } + return count; +} + + +/*########################################################################################################################* +*----------------------------------------------------Base mesh builder----------------------------------------------------* +*#########################################################################################################################*/ +static void AddSpriteVertices(BlockID block) { + int i = Atlas1D_Index(Block_Tex(block, FACE_XMAX)); + struct Builder1DPart* part = &Builder_Parts[i]; + part->sCount += 4 * 4; +} + +static void AddVertices(BlockID block, Face face) { + int baseOffset = (Blocks.Draw[block] == DRAW_TRANSLUCENT) * ATLAS1D_MAX_ATLASES; + int i = Atlas1D_Index(Block_Tex(block, face)); + struct Builder1DPart* part = &Builder_Parts[baseOffset + i]; + part->faces.count[face] += 4; +} + +#ifdef CC_BUILD_GL11 +static void BuildPartVbs(struct ChunkPartInfo* info) { + /* Sprites vertices are stored before chunk face sides */ + int i, count, offset = info->offset + info->spriteCount; + for (i = 0; i < FACE_COUNT; i++) { + count = info->counts[i]; + + if (count) { + info->vbs[i] = Gfx_CreateVb2(&Builder_Vertices[offset], VERTEX_FORMAT_TEXTURED, count); + offset += count; + } else { + info->vbs[i] = 0; + } + } + + count = info->spriteCount; + offset = info->offset; + if (count) { + info->vbs[i] = Gfx_CreateVb2(&Builder_Vertices[offset], VERTEX_FORMAT_TEXTURED, count); + } else { + info->vbs[i] = 0; + } +} +#endif + +static cc_bool SetPartInfo(struct Builder1DPart* part, int* offset, struct ChunkPartInfo* info) { + int vCount = Builder1DPart_VerticesCount(part); + info->offset = -1; + if (!vCount) return false; + + info->offset = *offset; + *offset += vCount; + + info->counts[FACE_XMIN] = part->faces.count[FACE_XMIN]; + info->counts[FACE_XMAX] = part->faces.count[FACE_XMAX]; + info->counts[FACE_ZMIN] = part->faces.count[FACE_ZMIN]; + info->counts[FACE_ZMAX] = part->faces.count[FACE_ZMAX]; + info->counts[FACE_YMIN] = part->faces.count[FACE_YMIN]; + info->counts[FACE_YMAX] = part->faces.count[FACE_YMAX]; + info->spriteCount = part->sCount; + return true; +} + + +static void PrepareChunk(int x1, int y1, int z1) { + int xMax = min(World.Width, x1 + CHUNK_SIZE); + int yMax = min(World.Height, y1 + CHUNK_SIZE); + int zMax = min(World.Length, z1 + CHUNK_SIZE); + + int cIndex, index, tileIdx; + BlockID b; + int x, y, z, xx, yy, zz; + +#ifdef OCCLUSION + int flags = ComputeOcclusion(); +#endif +#ifdef DEBUG_OCCLUSION + FastColour col = new FastColour(60, 60, 60, 255); + if (flags & 1) col.R = 255; /* x */ + if (flags & 4) col.G = 255; /* y */ + if (flags & 2) col.B = 255; /* z */ + map.Sunlight = map.Shadowlight = col; + map.SunlightXSide = map.ShadowlightXSide = col; + map.SunlightZSide = map.ShadowlightZSide = col; + map.SunlightYBottom = map.ShadowlightYBottom = col; +#endif + + for (y = y1, yy = 0; y < yMax; y++, yy++) { + for (z = z1, zz = 0; z < zMax; z++, zz++) { + cIndex = Builder_PackChunk(0, yy, zz); + + for (x = x1, xx = 0; x < xMax; x++, xx++, cIndex++) { + b = Builder_Chunk[cIndex]; + if (Blocks.Draw[b] == DRAW_GAS) continue; + index = Builder_PackCount(xx, yy, zz); + + /* Sprites can't be stretched, nor can then be they hidden by other blocks. */ + /* Note sprites are drawn using DrawSprite and not with any of the DrawXFace. */ + if (Blocks.Draw[b] == DRAW_SPRITE) { AddSpriteVertices(b); continue; } + + Builder_X = x; Builder_Y = y; Builder_Z = z; + Builder_FullBright = Blocks.Brightness[b]; + tileIdx = b * BLOCK_COUNT; + /* All of these function calls are inlined as they can be called tens of millions to hundreds of millions of times. */ + + if (Builder_Counts[index] == 0 || + (x == 0 && (y < Builder_SidesLevel || (b >= BLOCK_WATER && b <= BLOCK_STILL_LAVA && y < Builder_EdgeLevel))) || + (x != 0 && (Blocks.Hidden[tileIdx + Builder_Chunk[cIndex - 1]] & (1 << FACE_XMIN)) != 0)) { + Builder_Counts[index] = 0; + } else { + Builder_Counts[index] = Builder_StretchZ(index, x, y, z, cIndex, b, FACE_XMIN); + } + + index++; + if (Builder_Counts[index] == 0 || + (x == World.MaxX && (y < Builder_SidesLevel || (b >= BLOCK_WATER && b <= BLOCK_STILL_LAVA && y < Builder_EdgeLevel))) || + (x != World.MaxX && (Blocks.Hidden[tileIdx + Builder_Chunk[cIndex + 1]] & (1 << FACE_XMAX)) != 0)) { + Builder_Counts[index] = 0; + } else { + Builder_Counts[index] = Builder_StretchZ(index, x, y, z, cIndex, b, FACE_XMAX); + } + + index++; + if (Builder_Counts[index] == 0 || + (z == 0 && (y < Builder_SidesLevel || (b >= BLOCK_WATER && b <= BLOCK_STILL_LAVA && y < Builder_EdgeLevel))) || + (z != 0 && (Blocks.Hidden[tileIdx + Builder_Chunk[cIndex - EXTCHUNK_SIZE]] & (1 << FACE_ZMIN)) != 0)) { + Builder_Counts[index] = 0; + } else { + Builder_Counts[index] = Builder_StretchX(index, x, y, z, cIndex, b, FACE_ZMIN); + } + + index++; + if (Builder_Counts[index] == 0 || + (z == World.MaxZ && (y < Builder_SidesLevel || (b >= BLOCK_WATER && b <= BLOCK_STILL_LAVA && y < Builder_EdgeLevel))) || + (z != World.MaxZ && (Blocks.Hidden[tileIdx + Builder_Chunk[cIndex + EXTCHUNK_SIZE]] & (1 << FACE_ZMAX)) != 0)) { + Builder_Counts[index] = 0; + } else { + Builder_Counts[index] = Builder_StretchX(index, x, y, z, cIndex, b, FACE_ZMAX); + } + + index++; + if (Builder_Counts[index] == 0 || y == 0 || + (Blocks.Hidden[tileIdx + Builder_Chunk[cIndex - EXTCHUNK_SIZE_2]] & (1 << FACE_YMIN)) != 0) { + Builder_Counts[index] = 0; + } else { + Builder_Counts[index] = Builder_StretchX(index, x, y, z, cIndex, b, FACE_YMIN); + } + + index++; + if (Builder_Counts[index] == 0 || + (Blocks.Hidden[tileIdx + Builder_Chunk[cIndex + EXTCHUNK_SIZE_2]] & (1 << FACE_YMAX)) != 0) { + Builder_Counts[index] = 0; + } else if (b < BLOCK_WATER || b > BLOCK_STILL_LAVA) { + Builder_Counts[index] = Builder_StretchX(index, x, y, z, cIndex, b, FACE_YMAX); + } else { + Builder_Counts[index] = Builder_StretchXLiquid(index, x, y, z, cIndex, b); + } + } + } + } +} + +#define ReadChunkBody(get_block)\ +for (yy = -1; yy < 17; ++yy) {\ + y = yy + y1;\ + for (zz = -1; zz < 17; ++zz) {\ +\ + index = World_Pack(x1 - 1, y, z1 + zz);\ + cIndex = Builder_PackChunk(-1, yy, zz);\ + for (xx = -1; xx < 17; ++xx, ++index, ++cIndex) {\ +\ + block = get_block;\ + allAir = allAir && Blocks.Draw[block] == DRAW_GAS;\ + allSolid = allSolid && Blocks.FullOpaque[block];\ + Builder_Chunk[cIndex] = block;\ + }\ + }\ +} + +static cc_bool ReadChunkData(int x1, int y1, int z1, cc_bool* outAllAir) { + BlockRaw* blocks = World.Blocks; + BlockRaw* blocks2; + cc_bool allAir = true, allSolid = true; + int index, cIndex; + BlockID block; + int xx, yy, zz, y; + +#ifndef EXTENDED_BLOCKS + ReadChunkBody(blocks[index]); +#else + if (World.IDMask <= 0xFF) { + ReadChunkBody(blocks[index]); + } else { + blocks2 = World.Blocks2; + ReadChunkBody(blocks[index] | (blocks2[index] << 8)); + } +#endif + + *outAllAir = allAir; + return allSolid; +} + +#define ReadBorderChunkBody(get_block)\ +for (yy = -1; yy < 17; ++yy) {\ + y = yy + y1;\ + if (y < 0) continue;\ + if (y >= World.Height) break;\ +\ + for (zz = -1; zz < 17; ++zz) {\ + z = zz + z1;\ + if (z < 0) continue;\ + if (z >= World.Length) break;\ +\ + index = World_Pack(x1 - 1, y, z);\ + cIndex = Builder_PackChunk(-1, yy, zz);\ +\ + for (xx = -1; xx < 17; ++xx, ++index, ++cIndex) {\ + x = xx + x1;\ + if (x < 0) continue;\ + if (x >= World.Width) break;\ +\ + block = get_block;\ + allAir = allAir && Blocks.Draw[block] == DRAW_GAS;\ + Builder_Chunk[cIndex] = block;\ + }\ + }\ +} + +static cc_bool ReadBorderChunkData(int x1, int y1, int z1, cc_bool* outAllAir) { + BlockRaw* blocks = World.Blocks; + BlockRaw* blocks2; + cc_bool allAir = true; + int index, cIndex; + BlockID block; + int xx, yy, zz, x, y, z; + +#ifndef EXTENDED_BLOCKS + ReadBorderChunkBody(blocks[index]); +#else + if (World.IDMask <= 0xFF) { + ReadBorderChunkBody(blocks[index]); + } else { + blocks2 = World.Blocks2; + ReadBorderChunkBody(blocks[index] | (blocks2[index] << 8)); + } +#endif + + *outAllAir = allAir; + return false; +} + +static void OutputChunkPartsMeta(int x, int y, int z, struct ChunkInfo* info) { + cc_bool hasNorm, hasTran; + int partsIndex; + int i, j, curIdx, offset; + + partsIndex = World_ChunkPack(x >> CHUNK_SHIFT, y >> CHUNK_SHIFT, z >> CHUNK_SHIFT); + offset = 0; + hasNorm = false; + hasTran = false; + + for (i = 0; i < MapRenderer_1DUsedCount; i++) { + j = i + ATLAS1D_MAX_ATLASES; + curIdx = partsIndex + i * World.ChunksCount; + + hasNorm |= SetPartInfo(&Builder_Parts[i], &offset, &MapRenderer_PartsNormal[curIdx]); + hasTran |= SetPartInfo(&Builder_Parts[j], &offset, &MapRenderer_PartsTranslucent[curIdx]); + } + + if (hasNorm) { + info->normalParts = &MapRenderer_PartsNormal[partsIndex]; + } + if (hasTran) { + info->translucentParts = &MapRenderer_PartsTranslucent[partsIndex]; + } +} + +void Builder_MakeChunk(struct ChunkInfo* info) { +#ifdef CC_BUILD_SATURN + /* The Saturn build only has 16 kb stack, not large enough */ + static BlockID chunk[EXTCHUNK_SIZE_3]; + static cc_uint8 counts[CHUNK_SIZE_3 * FACE_COUNT]; + static int bitFlags[1]; +#else + BlockID chunk[EXTCHUNK_SIZE_3]; + cc_uint8 counts[CHUNK_SIZE_3 * FACE_COUNT]; + int bitFlags[EXTCHUNK_SIZE_3]; +#endif + + cc_bool allAir, allSolid, onBorder; + int xMax, yMax, zMax, totalVerts; + int cIndex, index; + int x, y, z, xx, yy, zz; + int x1 = info->centreX - 8, y1 = info->centreY - 8, z1 = info->centreZ - 8; + + Builder_Chunk = chunk; + Builder_Counts = counts; + Builder_BitFlags = bitFlags; + Builder_PrePrepareChunk(); + + onBorder = + x1 == 0 || y1 == 0 || z1 == 0 || x1 + CHUNK_SIZE >= World.Width || + y1 + CHUNK_SIZE >= World.Height || z1 + CHUNK_SIZE >= World.Length; + + if (onBorder) { + /* less optimal case here */ + Mem_Set(chunk, BLOCK_AIR, EXTCHUNK_SIZE_3 * sizeof(BlockID)); + allSolid = ReadBorderChunkData(x1, y1, z1, &allAir); + } else { + allSolid = ReadChunkData(x1, y1, z1, &allAir); + } + + info->allAir = allAir; + if (allAir || allSolid) return; + Lighting.LightHint(x1 - 1, y1 - 1, z1 - 1); + + Mem_Set(counts, 1, CHUNK_SIZE_3 * FACE_COUNT); + xMax = min(World.Width, x1 + CHUNK_SIZE); + yMax = min(World.Height, y1 + CHUNK_SIZE); + zMax = min(World.Length, z1 + CHUNK_SIZE); + + Builder_ChunkEndX = xMax; Builder_ChunkEndZ = zMax; + PrepareChunk(x1, y1, z1); + + totalVerts = Builder_TotalVerticesCount(); + if (!totalVerts) return; + + OutputChunkPartsMeta(x1, y1, z1, info); +#ifdef OCCLUSION + if (info.NormalParts != null || info.TranslucentParts != null) + info.occlusionFlags = (cc_uint8)ComputeOcclusion(); +#endif + +#ifndef CC_BUILD_GL11 + /* add an extra element to fix crashing on some GPUs */ + Builder_Vertices = (struct VertexTextured*)Gfx_RecreateAndLockVb(&info->vb, + VERTEX_FORMAT_TEXTURED, totalVerts + 1); +#else + /* NOTE: Relies on assumption vb is ignored by GL11 Gfx_LockVb implementation */ + Builder_Vertices = (struct VertexTextured*)Gfx_LockVb(0, + VERTEX_FORMAT_TEXTURED, totalVerts + 1); +#endif + Builder_PostPrepareChunk(); + /* now render the chunk */ + + for (y = y1, yy = 0; y < yMax; y++, yy++) { + for (z = z1, zz = 0; z < zMax; z++, zz++) { + cIndex = Builder_PackChunk(0, yy, zz); + + for (x = x1, xx = 0; x < xMax; x++, xx++, cIndex++) { + Builder_Block = chunk[cIndex]; + if (Blocks.Draw[Builder_Block] == DRAW_GAS) continue; + + index = Builder_PackCount(xx, yy, zz); + Builder_ChunkIndex = cIndex; + Builder_RenderBlock(index, x, y, z); + } + } + } + +#ifdef CC_BUILD_GL11 + cIndex = World_ChunkPack(x1 >> CHUNK_SHIFT, y1 >> CHUNK_SHIFT, z1 >> CHUNK_SHIFT); + + for (index = 0; index < MapRenderer_1DUsedCount; index++) { + int curIdx = cIndex + index * World.ChunksCount; + + BuildPartVbs(&MapRenderer_PartsNormal[curIdx]); + BuildPartVbs(&MapRenderer_PartsTranslucent[curIdx]); + } +#else + Gfx_UnlockVb(info->vb); +#endif +} + +static cc_bool Builder_OccludedLiquid(int chunkIndex) { + chunkIndex += EXTCHUNK_SIZE_2; /* Checking y above */ + return + Blocks.FullOpaque[Builder_Chunk[chunkIndex]] + && Blocks.Draw[Builder_Chunk[chunkIndex - EXTCHUNK_SIZE]] != DRAW_GAS + && Blocks.Draw[Builder_Chunk[chunkIndex - 1]] != DRAW_GAS + && Blocks.Draw[Builder_Chunk[chunkIndex + 1]] != DRAW_GAS + && Blocks.Draw[Builder_Chunk[chunkIndex + EXTCHUNK_SIZE]] != DRAW_GAS; +} + +static void DefaultPrePrepateChunk(void) { + Mem_Set(Builder_Parts, 0, sizeof(Builder_Parts)); +} + +static void DefaultPostStretchChunk(void) { + int i, j, offset; + offset = 0; + for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) { + j = i + ATLAS1D_MAX_ATLASES; + + offset = Builder1DPart_CalcOffsets(&Builder_Parts[i], offset); + offset = Builder1DPart_CalcOffsets(&Builder_Parts[j], offset); + } +} + +static RNGState spriteRng; +static void Builder_DrawSprite(int x, int y, int z) { + struct Builder1DPart* part; + struct VertexTextured* v; + cc_uint8 offsetType; + cc_bool bright; + PackedCol color; + TextureLoc loc; + float v1, v2; + + float X, Y, Z; + float valX, valY, valZ; + float x1,y1,z1, x2,y2,z2; + + X = (float)x; Y = (float)y; Z = (float)z; + x1 = X + 2.50f/16.0f; y1 = Y; z1 = Z + 2.50f/16.0f; + x2 = X + 13.5f/16.0f; y2 = Y + 1.0f; z2 = Z + 13.5f/16.0f; + +#define s_u1 0.0f +#define s_u2 UV2_Scale + loc = Block_Tex(Builder_Block, FACE_XMAX); + v1 = Atlas1D_RowId(loc) * Atlas1D.InvTileSize; + v2 = v1 + Atlas1D.InvTileSize * UV2_Scale; + + offsetType = Blocks.SpriteOffset[Builder_Block]; + if (offsetType >= 6 && offsetType <= 7) { + Random_Seed(&spriteRng, (x + 1217 * z) & 0x7fffffff); + valX = Random_Range(&spriteRng, -3, 3 + 1) / 16.0f; + valY = Random_Range(&spriteRng, 0, 3 + 1) / 16.0f; + valZ = Random_Range(&spriteRng, -3, 3 + 1) / 16.0f; + + x1 += valX - 1.7f/16.0f; x2 += valX + 1.7f/16.0f; + z1 += valZ - 1.7f/16.0f; z2 += valZ + 1.7f/16.0f; + if (offsetType == 7) { y1 -= valY; y2 -= valY; } + } + + bright = Blocks.Brightness[Builder_Block]; + part = &Builder_Parts[Atlas1D_Index(loc)]; + color = bright ? PACKEDCOL_WHITE : Lighting.Color_Sprite_Fast(x, y, z); + Block_Tint(color, Builder_Block); + + /* Draw Z axis */ + v = &Builder_Vertices[part->sOffset]; + v->x = x1; v->y = y1; v->z = z1; v->Col = color; v->U = s_u2; v->V = v2; v++; + v->x = x1; v->y = y2; v->z = z1; v->Col = color; v->U = s_u2; v->V = v1; v++; + v->x = x2; v->y = y2; v->z = z2; v->Col = color; v->U = s_u1; v->V = v1; v++; + v->x = x2; v->y = y1; v->z = z2; v->Col = color; v->U = s_u1; v->V = v2; v++; + + /* Draw Z axis mirrored */ + v -= 4; v += part->sCount >> 2; + v->x = x2; v->y = y1; v->z = z2; v->Col = color; v->U = s_u2; v->V = v2; v++; + v->x = x2; v->y = y2; v->z = z2; v->Col = color; v->U = s_u2; v->V = v1; v++; + v->x = x1; v->y = y2; v->z = z1; v->Col = color; v->U = s_u1; v->V = v1; v++; + v->x = x1; v->y = y1; v->z = z1; v->Col = color; v->U = s_u1; v->V = v2; v++; + + /* Draw X axis */ + v -= 4; v += part->sCount >> 2; + v->x = x1; v->y = y1; v->z = z2; v->Col = color; v->U = s_u2; v->V = v2; v++; + v->x = x1; v->y = y2; v->z = z2; v->Col = color; v->U = s_u2; v->V = v1; v++; + v->x = x2; v->y = y2; v->z = z1; v->Col = color; v->U = s_u1; v->V = v1; v++; + v->x = x2; v->y = y1; v->z = z1; v->Col = color; v->U = s_u1; v->V = v2; v++; + + /* Draw X axis mirrored */ + v -= 4; v += part->sCount >> 2; + v->x = x2; v->y = y1; v->z = z1; v->Col = color; v->U = s_u2; v->V = v2; v++; + v->x = x2; v->y = y2; v->z = z1; v->Col = color; v->U = s_u2; v->V = v1; v++; + v->x = x1; v->y = y2; v->z = z2; v->Col = color; v->U = s_u1; v->V = v1; v++; + v->x = x1; v->y = y1; v->z = z2; v->Col = color; v->U = s_u1; v->V = v2; v++; + + part->sOffset += 4; +} + + +/*########################################################################################################################* +*--------------------------------------------------Normal mesh builder----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol Normal_LightColor(int x, int y, int z, Face face, BlockID block) { + int offset = (Blocks.LightOffset[block] >> face) & 1; + + switch (face) { + case FACE_XMIN: + return x < offset ? Env.SunXSide : Lighting.Color_XSide_Fast(x - offset, y, z); + case FACE_XMAX: + return x > (World.MaxX - offset) ? Env.SunXSide : Lighting.Color_XSide_Fast(x + offset, y, z); + case FACE_ZMIN: + return z < offset ? Env.SunZSide : Lighting.Color_ZSide_Fast(x, y, z - offset); + case FACE_ZMAX: + return z > (World.MaxZ - offset) ? Env.SunZSide : Lighting.Color_ZSide_Fast(x, y, z + offset); + + case FACE_YMIN: + return Lighting.Color_YMin_Fast(x, y - offset, z); + case FACE_YMAX: + return Lighting.Color_YMax_Fast(x, y + offset, z); + } + return 0; /* should never happen */ +} + +static cc_bool Normal_CanStretch(BlockID initial, int chunkIndex, int x, int y, int z, Face face) { + BlockID cur = Builder_Chunk[chunkIndex]; + + if (cur != initial || Block_IsFaceHidden(cur, Builder_Chunk[chunkIndex + Builder_Offsets[face]], face)) return false; + if (Builder_FullBright) return true; + + return Normal_LightColor(Builder_X, Builder_Y, Builder_Z, face, initial) == Normal_LightColor(x, y, z, face, cur); +} + +static int NormalBuilder_StretchXLiquid(int countIndex, int x, int y, int z, int chunkIndex, BlockID block) { + int count = 1; cc_bool stretchTile; + if (Builder_OccludedLiquid(chunkIndex)) return 0; + + x++; + chunkIndex++; + countIndex += FACE_COUNT; + stretchTile = (Blocks.CanStretch[block] & (1 << FACE_YMAX)) != 0; + + while (x < Builder_ChunkEndX && stretchTile && Normal_CanStretch(block, chunkIndex, x, y, z, FACE_YMAX) && !Builder_OccludedLiquid(chunkIndex)) { + Builder_Counts[countIndex] = 0; + count++; + x++; + chunkIndex++; + countIndex += FACE_COUNT; + } + AddVertices(block, FACE_YMAX); + return count; +} + +static int NormalBuilder_StretchX(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face) { + int count = 1; cc_bool stretchTile; + x++; + chunkIndex++; + countIndex += FACE_COUNT; + stretchTile = (Blocks.CanStretch[block] & (1 << face)) != 0; + + while (x < Builder_ChunkEndX && stretchTile && Normal_CanStretch(block, chunkIndex, x, y, z, face)) { + Builder_Counts[countIndex] = 0; + count++; + x++; + chunkIndex++; + countIndex += FACE_COUNT; + } + AddVertices(block, face); + return count; +} + +static int NormalBuilder_StretchZ(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face) { + int count = 1; cc_bool stretchTile; + z++; + chunkIndex += EXTCHUNK_SIZE; + countIndex += CHUNK_SIZE * FACE_COUNT; + stretchTile = (Blocks.CanStretch[block] & (1 << face)) != 0; + + while (z < Builder_ChunkEndZ && stretchTile && Normal_CanStretch(block, chunkIndex, x, y, z, face)) { + Builder_Counts[countIndex] = 0; + count++; + z++; + chunkIndex += EXTCHUNK_SIZE; + countIndex += CHUNK_SIZE * FACE_COUNT; + } + AddVertices(block, face); + return count; +} + +static void NormalBuilder_RenderBlock(int index, int x, int y, int z) { + /* counters */ + int count_XMin, count_XMax, count_ZMin; + int count_ZMax, count_YMin, count_YMax; + + /* block state */ + Vec3 min, max; + int baseOffset, lightFlags; + cc_bool fullBright; + + /* per-face state */ + struct Builder1DPart* part; + TextureLoc loc; + PackedCol col; + int offset; + + if (Blocks.Draw[Builder_Block] == DRAW_SPRITE) { + Builder_DrawSprite(x, y, z); return; + } + + count_XMin = Builder_Counts[index + FACE_XMIN]; + count_XMax = Builder_Counts[index + FACE_XMAX]; + count_ZMin = Builder_Counts[index + FACE_ZMIN]; + count_ZMax = Builder_Counts[index + FACE_ZMAX]; + count_YMin = Builder_Counts[index + FACE_YMIN]; + count_YMax = Builder_Counts[index + FACE_YMAX]; + + if (!count_XMin && !count_XMax && !count_ZMin && + !count_ZMax && !count_YMin && !count_YMax) return; + + fullBright = Blocks.Brightness[Builder_Block]; + baseOffset = (Blocks.Draw[Builder_Block] == DRAW_TRANSLUCENT) * ATLAS1D_MAX_ATLASES; + lightFlags = Blocks.LightOffset[Builder_Block]; + + Drawer.MinBB = Blocks.MinBB[Builder_Block]; Drawer.MinBB.y = 1.0f - Drawer.MinBB.y; + Drawer.MaxBB = Blocks.MaxBB[Builder_Block]; Drawer.MaxBB.y = 1.0f - Drawer.MaxBB.y; + + min = Blocks.RenderMinBB[Builder_Block]; max = Blocks.RenderMaxBB[Builder_Block]; + Drawer.X1 = x + min.x; Drawer.Y1 = y + min.y; Drawer.Z1 = z + min.z; + Drawer.X2 = x + max.x; Drawer.Y2 = y + max.y; Drawer.Z2 = z + max.z; + + Drawer.Tinted = Blocks.Tinted[Builder_Block]; + Drawer.TintCol = Blocks.FogCol[Builder_Block]; + + if (count_XMin) { + loc = Block_Tex(Builder_Block, FACE_XMIN); + offset = (lightFlags >> FACE_XMIN) & 1; + part = &Builder_Parts[baseOffset + Atlas1D_Index(loc)]; + + col = fullBright ? PACKEDCOL_WHITE : + x >= offset ? Lighting.Color_XSide_Fast(x - offset, y, z) : Env.SunXSide; + Drawer_XMin(count_XMin, col, loc, &part->faces.vertices[FACE_XMIN]); + } + + if (count_XMax) { + loc = Block_Tex(Builder_Block, FACE_XMAX); + offset = (lightFlags >> FACE_XMAX) & 1; + part = &Builder_Parts[baseOffset + Atlas1D_Index(loc)]; + + col = fullBright ? PACKEDCOL_WHITE : + x <= (World.MaxX - offset) ? Lighting.Color_XSide_Fast(x + offset, y, z) : Env.SunXSide; + Drawer_XMax(count_XMax, col, loc, &part->faces.vertices[FACE_XMAX]); + } + + if (count_ZMin) { + loc = Block_Tex(Builder_Block, FACE_ZMIN); + offset = (lightFlags >> FACE_ZMIN) & 1; + part = &Builder_Parts[baseOffset + Atlas1D_Index(loc)]; + + col = fullBright ? PACKEDCOL_WHITE : + z >= offset ? Lighting.Color_ZSide_Fast(x, y, z - offset) : Env.SunZSide; + Drawer_ZMin(count_ZMin, col, loc, &part->faces.vertices[FACE_ZMIN]); + } + + if (count_ZMax) { + loc = Block_Tex(Builder_Block, FACE_ZMAX); + offset = (lightFlags >> FACE_ZMAX) & 1; + part = &Builder_Parts[baseOffset + Atlas1D_Index(loc)]; + + col = fullBright ? PACKEDCOL_WHITE : + z <= (World.MaxZ - offset) ? Lighting.Color_ZSide_Fast(x, y, z + offset) : Env.SunZSide; + Drawer_ZMax(count_ZMax, col, loc, &part->faces.vertices[FACE_ZMAX]); + } + + if (count_YMin) { + loc = Block_Tex(Builder_Block, FACE_YMIN); + offset = (lightFlags >> FACE_YMIN) & 1; + part = &Builder_Parts[baseOffset + Atlas1D_Index(loc)]; + + col = fullBright ? PACKEDCOL_WHITE : Lighting.Color_YMin_Fast(x, y - offset, z); + Drawer_YMin(count_YMin, col, loc, &part->faces.vertices[FACE_YMIN]); + } + + if (count_YMax) { + loc = Block_Tex(Builder_Block, FACE_YMAX); + offset = (lightFlags >> FACE_YMAX) & 1; + part = &Builder_Parts[baseOffset + Atlas1D_Index(loc)]; + + col = fullBright ? PACKEDCOL_WHITE : Lighting.Color_YMax_Fast(x, y + offset, z); + Drawer_YMax(count_YMax, col, loc, &part->faces.vertices[FACE_YMAX]); + } +} + +static void Builder_SetDefault(void) { + Builder_StretchXLiquid = NULL; + Builder_StretchX = NULL; + Builder_StretchZ = NULL; + Builder_RenderBlock = NULL; + + Builder_PrePrepareChunk = DefaultPrePrepateChunk; + Builder_PostPrepareChunk = DefaultPostStretchChunk; +} + +static void NormalBuilder_SetActive(void) { + Builder_SetDefault(); + Builder_StretchXLiquid = NormalBuilder_StretchXLiquid; + Builder_StretchX = NormalBuilder_StretchX; + Builder_StretchZ = NormalBuilder_StretchZ; + Builder_RenderBlock = NormalBuilder_RenderBlock; +} + + +/*########################################################################################################################* +*-------------------------------------------------Advanced mesh builder---------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_ADVLIGHTING +static Vec3 adv_minBB, adv_maxBB; +static int adv_initBitFlags, adv_baseOffset; +static int* adv_bitFlags; +static float adv_x1, adv_y1, adv_z1, adv_x2, adv_y2, adv_z2; +static PackedCol adv_lerp[5], adv_lerpX[5], adv_lerpZ[5], adv_lerpY[5]; +static cc_bool adv_tinted; + +enum ADV_MASK { + /* z-1 cube points */ + xM1_yM1_zM1, xM1_yCC_zM1, xM1_yP1_zM1, + xCC_yM1_zM1, xCC_yCC_zM1, xCC_yP1_zM1, + xP1_yM1_zM1, xP1_yCC_zM1, xP1_yP1_zM1, + /* z cube points */ + xM1_yM1_zCC, xM1_yCC_zCC, xM1_yP1_zCC, + xCC_yM1_zCC, xCC_yCC_zCC, xCC_yP1_zCC, + xP1_yM1_zCC, xP1_yCC_zCC, xP1_yP1_zCC, + /* z+1 cube points */ + xM1_yM1_zP1, xM1_yCC_zP1, xM1_yP1_zP1, + xCC_yM1_zP1, xCC_yCC_zP1, xCC_yP1_zP1, + xP1_yM1_zP1, xP1_yCC_zP1, xP1_yP1_zP1, +}; + +/* Bit-or the Adv_Lit flags with these to set the appropriate light values */ +#define LIT_M1 (1 << 0) +#define LIT_CC (1 << 1) +#define LIT_P1 (1 << 2) +/* Returns a 3 bit value where */ +/* - bit 0 set: Y-1 is in light */ +/* - bit 1 set: Y is in light */ +/* - bit 2 set: Y+1 is in light */ +static int Adv_Lit(int x, int y, int z, int cIndex) { + int flags, offset, lightFlags; + BlockID block; + if (y < 0 || y >= World.Height) return LIT_M1 | LIT_CC | LIT_P1; /* all faces lit */ + + /* TODO: check sides height (if sides > edges), check if edge block casts a shadow */ + if (!World_ContainsXZ(x, z)) { + return y >= Builder_EdgeLevel ? LIT_M1 | LIT_CC | LIT_P1 : y == (Builder_EdgeLevel - 1) ? LIT_CC | LIT_P1 : 0; + } + + flags = 0; + block = Builder_Chunk[cIndex]; + lightFlags = Blocks.LightOffset[block]; + + /* TODO using LIGHT_FLAG_SHADES_FROM_BELOW is wrong here, */ + /* but still produces less broken results than YMIN/YMAX */ + + /* Use fact Light(Y.YMin) == Light((Y-1).YMax) */ + offset = (lightFlags >> LIGHT_FLAG_SHADES_FROM_BELOW) & 1; + flags |= Lighting.IsLit_Fast(x, y - offset, z) ? LIT_M1 : 0; + + /* Light is same for all the horizontal faces */ + flags |= Lighting.IsLit_Fast(x, y, z) ? LIT_CC : 0; + + /* Use fact Light((Y+1).YMin) == Light(Y.YMax) */ + offset = (lightFlags >> LIGHT_FLAG_SHADES_FROM_BELOW) & 1; + flags |= Lighting.IsLit_Fast(x, (y + 1) - offset, z) ? LIT_P1 : 0; + + /* If a block is fullbright, it should also look as if that spot is lit */ + if (Blocks.Brightness[Builder_Chunk[cIndex - 324]]) flags |= LIT_M1; + if (Blocks.Brightness[block]) flags |= LIT_CC; + if (Blocks.Brightness[Builder_Chunk[cIndex + 324]]) flags |= LIT_P1; + + return flags; +} + +static int Adv_ComputeLightFlags(int x, int y, int z, int cIndex) { + if (Builder_FullBright) return (1 << xP1_yP1_zP1) - 1; /* all faces fully bright */ + + return + Adv_Lit(x - 1, y, z - 1, cIndex - 1 - 18) << xM1_yM1_zM1 | + Adv_Lit(x - 1, y, z, cIndex - 1) << xM1_yM1_zCC | + Adv_Lit(x - 1, y, z + 1, cIndex - 1 + 18) << xM1_yM1_zP1 | + Adv_Lit(x, y, z - 1, cIndex + 0 - 18) << xCC_yM1_zM1 | + Adv_Lit(x, y, z, cIndex + 0) << xCC_yM1_zCC | + Adv_Lit(x, y, z + 1, cIndex + 0 + 18) << xCC_yM1_zP1 | + Adv_Lit(x + 1, y, z - 1, cIndex + 1 - 18) << xP1_yM1_zM1 | + Adv_Lit(x + 1, y, z, cIndex + 1) << xP1_yM1_zCC | + Adv_Lit(x + 1, y, z + 1, cIndex + 1 + 18) << xP1_yM1_zP1; +} + +static int adv_masks[FACE_COUNT] = { + /* XMin face */ + (1 << xM1_yM1_zM1) | (1 << xM1_yM1_zCC) | (1 << xM1_yM1_zP1) | + (1 << xM1_yCC_zM1) | (1 << xM1_yCC_zCC) | (1 << xM1_yCC_zP1) | + (1 << xM1_yP1_zM1) | (1 << xM1_yP1_zCC) | (1 << xM1_yP1_zP1), + /* XMax face */ + (1 << xP1_yM1_zM1) | (1 << xP1_yM1_zCC) | (1 << xP1_yM1_zP1) | + (1 << xP1_yP1_zM1) | (1 << xP1_yP1_zCC) | (1 << xP1_yP1_zP1) | + (1 << xP1_yCC_zM1) | (1 << xP1_yCC_zCC) | (1 << xP1_yCC_zP1), + /* ZMin face */ + (1 << xM1_yM1_zM1) | (1 << xCC_yM1_zM1) | (1 << xP1_yM1_zM1) | + (1 << xM1_yCC_zM1) | (1 << xCC_yCC_zM1) | (1 << xP1_yCC_zM1) | + (1 << xM1_yP1_zM1) | (1 << xCC_yP1_zM1) | (1 << xP1_yP1_zM1), + /* ZMax face */ + (1 << xM1_yM1_zP1) | (1 << xCC_yM1_zP1) | (1 << xP1_yM1_zP1) | + (1 << xM1_yCC_zP1) | (1 << xCC_yCC_zP1) | (1 << xP1_yCC_zP1) | + (1 << xM1_yP1_zP1) | (1 << xCC_yP1_zP1) | (1 << xP1_yP1_zP1), + /* YMin face */ + (1 << xM1_yM1_zM1) | (1 << xM1_yM1_zCC) | (1 << xM1_yM1_zP1) | + (1 << xCC_yM1_zM1) | (1 << xCC_yM1_zCC) | (1 << xCC_yM1_zP1) | + (1 << xP1_yM1_zM1) | (1 << xP1_yM1_zCC) | (1 << xP1_yM1_zP1), + /* YMax face */ + (1 << xM1_yP1_zM1) | (1 << xM1_yP1_zCC) | (1 << xM1_yP1_zP1) | + (1 << xCC_yP1_zM1) | (1 << xCC_yP1_zCC) | (1 << xCC_yP1_zP1) | + (1 << xP1_yP1_zM1) | (1 << xP1_yP1_zCC) | (1 << xP1_yP1_zP1), +}; + + +static cc_bool Adv_CanStretch(BlockID initial, int chunkIndex, int x, int y, int z, Face face) { + BlockID cur = Builder_Chunk[chunkIndex]; + adv_bitFlags[chunkIndex] = Adv_ComputeLightFlags(x, y, z, chunkIndex); + + return cur == initial + && !Block_IsFaceHidden(cur, Builder_Chunk[chunkIndex + Builder_Offsets[face]], face) + && (adv_initBitFlags == adv_bitFlags[chunkIndex] + /* Check that this face is either fully bright or fully in shadow */ + && (adv_initBitFlags == 0 || (adv_initBitFlags & adv_masks[face]) == adv_masks[face])); +} + +static int Adv_StretchXLiquid(int countIndex, int x, int y, int z, int chunkIndex, BlockID block) { + int count = 1; cc_bool stretchTile; + if (Builder_OccludedLiquid(chunkIndex)) return 0; + adv_initBitFlags = Adv_ComputeLightFlags(x, y, z, chunkIndex); + adv_bitFlags[chunkIndex] = adv_initBitFlags; + + x++; + chunkIndex++; + countIndex += FACE_COUNT; + stretchTile = (Blocks.CanStretch[block] & (1 << FACE_YMAX)) != 0; + + while (x < Builder_ChunkEndX && stretchTile && Adv_CanStretch(block, chunkIndex, x, y, z, FACE_YMAX) && !Builder_OccludedLiquid(chunkIndex)) { + Builder_Counts[countIndex] = 0; + count++; + x++; + chunkIndex++; + countIndex += FACE_COUNT; + } + AddVertices(block, FACE_YMAX); + return count; +} + +static int Adv_StretchX(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face) { + int count = 1; cc_bool stretchTile; + adv_initBitFlags = Adv_ComputeLightFlags(x, y, z, chunkIndex); + adv_bitFlags[chunkIndex] = adv_initBitFlags; + + x++; + chunkIndex++; + countIndex += FACE_COUNT; + stretchTile = (Blocks.CanStretch[block] & (1 << face)) != 0; + + while (x < Builder_ChunkEndX && stretchTile && Adv_CanStretch(block, chunkIndex, x, y, z, face)) { + Builder_Counts[countIndex] = 0; + count++; + x++; + chunkIndex++; + countIndex += FACE_COUNT; + } + AddVertices(block, face); + return count; +} + +static int Adv_StretchZ(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face) { + int count = 1; cc_bool stretchTile; + adv_initBitFlags = Adv_ComputeLightFlags(x, y, z, chunkIndex); + adv_bitFlags[chunkIndex] = adv_initBitFlags; + + z++; + chunkIndex += EXTCHUNK_SIZE; + countIndex += CHUNK_SIZE * FACE_COUNT; + stretchTile = (Blocks.CanStretch[block] & (1 << face)) != 0; + + while (z < Builder_ChunkEndZ && stretchTile && Adv_CanStretch(block, chunkIndex, x, y, z, face)) { + Builder_Counts[countIndex] = 0; + count++; + z++; + chunkIndex += EXTCHUNK_SIZE; + countIndex += CHUNK_SIZE * FACE_COUNT; + } + AddVertices(block, face); + return count; +} + + +#define Adv_CountBits(F, a, b, c, d) (((F >> a) & 1) + ((F >> b) & 1) + ((F >> c) & 1) + ((F >> d) & 1)) + +static void Adv_DrawXMin(int count) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_XMIN); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.z, u2 = (count - 1) + adv_maxBB.z * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + int F = adv_bitFlags[Builder_ChunkIndex]; + int aY0_Z0 = Adv_CountBits(F, xM1_yM1_zM1, xM1_yCC_zM1, xM1_yM1_zCC, xM1_yCC_zCC); + int aY0_Z1 = Adv_CountBits(F, xM1_yM1_zP1, xM1_yCC_zP1, xM1_yM1_zCC, xM1_yCC_zCC); + int aY1_Z0 = Adv_CountBits(F, xM1_yP1_zM1, xM1_yCC_zM1, xM1_yP1_zCC, xM1_yCC_zCC); + int aY1_Z1 = Adv_CountBits(F, xM1_yP1_zP1, xM1_yCC_zP1, xM1_yP1_zCC, xM1_yCC_zCC); + + PackedCol tint, white = PACKEDCOL_WHITE; + PackedCol col0_0 = Builder_FullBright ? white : adv_lerpX[aY0_Z0], col1_0 = Builder_FullBright ? white : adv_lerpX[aY1_Z0]; + PackedCol col1_1 = Builder_FullBright ? white : adv_lerpX[aY1_Z1], col0_1 = Builder_FullBright ? white : adv_lerpX[aY0_Z1]; + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_XMIN]; + v.x = adv_x1; + if (aY0_Z0 + aY1_Z1 > aY0_Z1 + aY1_Z0) { + v.y = adv_y2; v.z = adv_z1; v.U = u1; v.V = v1; v.Col = col1_0; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.z = adv_z2 + (count - 1); v.U = u2; v.Col = col0_1; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col1_1; *vertices++ = v; + } else { + v.y = adv_y2; v.z = adv_z2 + (count - 1); v.U = u2; v.V = v1; v.Col = col1_1; *vertices++ = v; + v.z = adv_z1; v.U = u1; v.Col = col1_0; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.z = adv_z2 + (count - 1); v.U = u2; v.Col = col0_1; *vertices++ = v; + } + part->faces.vertices[FACE_XMIN] = vertices; +} + +static void Adv_DrawXMax(int count) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_XMAX); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = (count - adv_minBB.z), u2 = (1 - adv_maxBB.z) * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + int F = adv_bitFlags[Builder_ChunkIndex]; + int aY0_Z0 = Adv_CountBits(F, xP1_yM1_zM1, xP1_yCC_zM1, xP1_yM1_zCC, xP1_yCC_zCC); + int aY0_Z1 = Adv_CountBits(F, xP1_yM1_zP1, xP1_yCC_zP1, xP1_yM1_zCC, xP1_yCC_zCC); + int aY1_Z0 = Adv_CountBits(F, xP1_yP1_zM1, xP1_yCC_zM1, xP1_yP1_zCC, xP1_yCC_zCC); + int aY1_Z1 = Adv_CountBits(F, xP1_yP1_zP1, xP1_yCC_zP1, xP1_yP1_zCC, xP1_yCC_zCC); + + PackedCol tint, white = PACKEDCOL_WHITE; + PackedCol col0_0 = Builder_FullBright ? white : adv_lerpX[aY0_Z0], col1_0 = Builder_FullBright ? white : adv_lerpX[aY1_Z0]; + PackedCol col1_1 = Builder_FullBright ? white : adv_lerpX[aY1_Z1], col0_1 = Builder_FullBright ? white : adv_lerpX[aY0_Z1]; + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_XMAX]; + v.x = adv_x2; + if (aY0_Z0 + aY1_Z1 > aY0_Z1 + aY1_Z0) { + v.y = adv_y2; v.z = adv_z1; v.U = u1; v.V = v1; v.Col = col1_0; *vertices++ = v; + v.z = adv_z2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.z = adv_z1; v.U = u1; v.Col = col0_0; *vertices++ = v; + } else { + v.y = adv_y2; v.z = adv_z2 + (count - 1); v.U = u2; v.V = v1; v.Col = col1_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.z = adv_z1; v.U = u1; v.Col = col0_0; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col1_0; *vertices++ = v; + } + part->faces.vertices[FACE_XMAX] = vertices; +} + +static void Adv_DrawZMin(int count) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_ZMIN); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = (count - adv_minBB.x), u2 = (1 - adv_maxBB.x) * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + int F = adv_bitFlags[Builder_ChunkIndex]; + int aX0_Y0 = Adv_CountBits(F, xM1_yM1_zM1, xM1_yCC_zM1, xCC_yM1_zM1, xCC_yCC_zM1); + int aX0_Y1 = Adv_CountBits(F, xM1_yP1_zM1, xM1_yCC_zM1, xCC_yP1_zM1, xCC_yCC_zM1); + int aX1_Y0 = Adv_CountBits(F, xP1_yM1_zM1, xP1_yCC_zM1, xCC_yM1_zM1, xCC_yCC_zM1); + int aX1_Y1 = Adv_CountBits(F, xP1_yP1_zM1, xP1_yCC_zM1, xCC_yP1_zM1, xCC_yCC_zM1); + + PackedCol tint, white = PACKEDCOL_WHITE; + PackedCol col0_0 = Builder_FullBright ? white : adv_lerpZ[aX0_Y0], col1_0 = Builder_FullBright ? white : adv_lerpZ[aX1_Y0]; + PackedCol col1_1 = Builder_FullBright ? white : adv_lerpZ[aX1_Y1], col0_1 = Builder_FullBright ? white : adv_lerpZ[aX0_Y1]; + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_ZMIN]; + v.z = adv_z1; + if (aX1_Y1 + aX0_Y0 > aX0_Y1 + aX1_Y0) { + v.x = adv_x2 + (count - 1); v.y = adv_y1; v.U = u2; v.V = v2; v.Col = col1_0; *vertices++ = v; + v.x = adv_x1; v.U = u1; v.Col = col0_0; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col0_1; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + } else { + v.x = adv_x1; v.y = adv_y1; v.U = u1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col0_1; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col1_0; *vertices++ = v; + } + part->faces.vertices[FACE_ZMIN] = vertices; +} + +static void Adv_DrawZMax(int count) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_ZMAX); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.x, u2 = (count - 1) + adv_maxBB.x * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + int F = adv_bitFlags[Builder_ChunkIndex]; + int aX0_Y0 = Adv_CountBits(F, xM1_yM1_zP1, xM1_yCC_zP1, xCC_yM1_zP1, xCC_yCC_zP1); + int aX1_Y0 = Adv_CountBits(F, xP1_yM1_zP1, xP1_yCC_zP1, xCC_yM1_zP1, xCC_yCC_zP1); + int aX0_Y1 = Adv_CountBits(F, xM1_yP1_zP1, xM1_yCC_zP1, xCC_yP1_zP1, xCC_yCC_zP1); + int aX1_Y1 = Adv_CountBits(F, xP1_yP1_zP1, xP1_yCC_zP1, xCC_yP1_zP1, xCC_yCC_zP1); + + PackedCol tint, white = PACKEDCOL_WHITE; + PackedCol col1_1 = Builder_FullBright ? white : adv_lerpZ[aX1_Y1], col1_0 = Builder_FullBright ? white : adv_lerpZ[aX1_Y0]; + PackedCol col0_0 = Builder_FullBright ? white : adv_lerpZ[aX0_Y0], col0_1 = Builder_FullBright ? white : adv_lerpZ[aX0_Y1]; + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_ZMAX]; + v.z = adv_z2; + if (aX1_Y1 + aX0_Y0 > aX0_Y1 + aX1_Y0) { + v.x = adv_x1; v.y = adv_y2; v.U = u1; v.V = v1; v.Col = col0_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_0; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col1_1; *vertices++ = v; + } else { + v.x = adv_x2 + (count - 1); v.y = adv_y2; v.U = u2; v.V = v1; v.Col = col1_1; *vertices++ = v; + v.x = adv_x1; v.U = u1; v.Col = col0_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_0; *vertices++ = v; + } + part->faces.vertices[FACE_ZMAX] = vertices; +} + +static void Adv_DrawYMin(int count) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_YMIN); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.x, u2 = (count - 1) + adv_maxBB.x * UV2_Scale; + float v1 = vOrigin + adv_minBB.z * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_maxBB.z * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + int F = adv_bitFlags[Builder_ChunkIndex]; + int aX0_Z0 = Adv_CountBits(F, xM1_yM1_zM1, xM1_yM1_zCC, xCC_yM1_zM1, xCC_yM1_zCC); + int aX1_Z0 = Adv_CountBits(F, xP1_yM1_zM1, xP1_yM1_zCC, xCC_yM1_zM1, xCC_yM1_zCC); + int aX0_Z1 = Adv_CountBits(F, xM1_yM1_zP1, xM1_yM1_zCC, xCC_yM1_zP1, xCC_yM1_zCC); + int aX1_Z1 = Adv_CountBits(F, xP1_yM1_zP1, xP1_yM1_zCC, xCC_yM1_zP1, xCC_yM1_zCC); + + PackedCol tint, white = PACKEDCOL_WHITE; + PackedCol col0_1 = Builder_FullBright ? white : adv_lerpY[aX0_Z1], col1_1 = Builder_FullBright ? white : adv_lerpY[aX1_Z1]; + PackedCol col1_0 = Builder_FullBright ? white : adv_lerpY[aX1_Z0], col0_0 = Builder_FullBright ? white : adv_lerpY[aX0_Z0]; + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_YMIN]; + v.y = adv_y1; + if (aX0_Z1 + aX1_Z0 > aX0_Z0 + aX1_Z1) { + v.x = adv_x2 + (count - 1); v.z = adv_z2; v.U = u2; v.V = v2; v.Col = col1_1; *vertices++ = v; + v.x = adv_x1; v.U = u1; v.Col = col0_1; *vertices++ = v; + v.z = adv_z1; v.V = v1; v.Col = col0_0; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_0; *vertices++ = v; + } else { + v.x = adv_x1; v.z = adv_z2; v.U = u1; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.z = adv_z1; v.V = v1; v.Col = col0_0; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_0; *vertices++ = v; + v.z = adv_z2; v.V = v2; v.Col = col1_1; *vertices++ = v; + } + part->faces.vertices[FACE_YMIN] = vertices; +} + +static void Adv_DrawYMax(int count) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_YMAX); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.x, u2 = (count - 1) + adv_maxBB.x * UV2_Scale; + float v1 = vOrigin + adv_minBB.z * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_maxBB.z * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + int F = adv_bitFlags[Builder_ChunkIndex]; + int aX0_Z0 = Adv_CountBits(F, xM1_yP1_zM1, xM1_yP1_zCC, xCC_yP1_zM1, xCC_yP1_zCC); + int aX1_Z0 = Adv_CountBits(F, xP1_yP1_zM1, xP1_yP1_zCC, xCC_yP1_zM1, xCC_yP1_zCC); + int aX0_Z1 = Adv_CountBits(F, xM1_yP1_zP1, xM1_yP1_zCC, xCC_yP1_zP1, xCC_yP1_zCC); + int aX1_Z1 = Adv_CountBits(F, xP1_yP1_zP1, xP1_yP1_zCC, xCC_yP1_zP1, xCC_yP1_zCC); + + PackedCol tint, white = PACKEDCOL_WHITE; + PackedCol col0_0 = Builder_FullBright ? white : adv_lerp[aX0_Z0], col1_0 = Builder_FullBright ? white : adv_lerp[aX1_Z0]; + PackedCol col1_1 = Builder_FullBright ? white : adv_lerp[aX1_Z1], col0_1 = Builder_FullBright ? white : adv_lerp[aX0_Z1]; + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_YMAX]; + v.y = adv_y2; + if (aX0_Z0 + aX1_Z1 > aX0_Z1 + aX1_Z0) { + v.x = adv_x2 + (count - 1); v.z = adv_z1; v.U = u2; v.V = v1; v.Col = col1_0; *vertices++ = v; + v.x = adv_x1; v.U = u1; v.Col = col0_0; *vertices++ = v; + v.z = adv_z2; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + } else { + v.x = adv_x1; v.z = adv_z1; v.U = u1; v.V = v1; v.Col = col0_0; *vertices++ = v; + v.z = adv_z2; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + v.z = adv_z1; v.V = v1; v.Col = col1_0; *vertices++ = v; + } + part->faces.vertices[FACE_YMAX] = vertices; +} + +static void Adv_RenderBlock(int index, int x, int y, int z) { + Vec3 min, max; + int count_XMin, count_XMax, count_ZMin; + int count_ZMax, count_YMin, count_YMax; + + if (Blocks.Draw[Builder_Block] == DRAW_SPRITE) { + Builder_DrawSprite(x, y, z); return; + } + + count_XMin = Builder_Counts[index + FACE_XMIN]; + count_XMax = Builder_Counts[index + FACE_XMAX]; + count_ZMin = Builder_Counts[index + FACE_ZMIN]; + count_ZMax = Builder_Counts[index + FACE_ZMAX]; + count_YMin = Builder_Counts[index + FACE_YMIN]; + count_YMax = Builder_Counts[index + FACE_YMAX]; + + if (!count_XMin && !count_XMax && !count_ZMin && + !count_ZMax && !count_YMin && !count_YMax) return; + + Builder_FullBright = Blocks.Brightness[Builder_Block]; + adv_baseOffset = (Blocks.Draw[Builder_Block] == DRAW_TRANSLUCENT) * ATLAS1D_MAX_ATLASES; + adv_tinted = Blocks.Tinted[Builder_Block]; + + min = Blocks.RenderMinBB[Builder_Block]; max = Blocks.RenderMaxBB[Builder_Block]; + adv_x1 = x + min.x; adv_y1 = y + min.y; adv_z1 = z + min.z; + adv_x2 = x + max.x; adv_y2 = y + max.y; adv_z2 = z + max.z; + + adv_minBB = Blocks.MinBB[Builder_Block]; adv_maxBB = Blocks.MaxBB[Builder_Block]; + adv_minBB.y = 1.0f - adv_minBB.y; adv_maxBB.y = 1.0f - adv_maxBB.y; + + if (count_XMin) Adv_DrawXMin(count_XMin); + if (count_XMax) Adv_DrawXMax(count_XMax); + if (count_ZMin) Adv_DrawZMin(count_ZMin); + if (count_ZMax) Adv_DrawZMax(count_ZMax); + if (count_YMin) Adv_DrawYMin(count_YMin); + if (count_YMax) Adv_DrawYMax(count_YMax); +} + +static void Adv_PrePrepareChunk(void) { + int i; + DefaultPrePrepateChunk(); + adv_bitFlags = Builder_BitFlags; + + for (i = 0; i <= 4; i++) { + adv_lerp[i] = PackedCol_Lerp(Env.ShadowCol, Env.SunCol, i / 4.0f); + adv_lerpX[i] = PackedCol_Lerp(Env.ShadowXSide, Env.SunXSide, i / 4.0f); + adv_lerpZ[i] = PackedCol_Lerp(Env.ShadowZSide, Env.SunZSide, i / 4.0f); + adv_lerpY[i] = PackedCol_Lerp(Env.ShadowYMin, Env.SunYMin, i / 4.0f); + } +} + +static void AdvBuilder_SetActive(void) { + Builder_SetDefault(); + Builder_StretchXLiquid = Adv_StretchXLiquid; + Builder_StretchX = Adv_StretchX; + Builder_StretchZ = Adv_StretchZ; + Builder_RenderBlock = Adv_RenderBlock; + Builder_PrePrepareChunk = Adv_PrePrepareChunk; +} +#else +static void AdvBuilder_SetActive(void) { NormalBuilder_SetActive(); } +#endif + + + +/*########################################################################################################################* +*-------------------------------------------------Modern mesh builder-----------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_ADVLIGHTING +/* Fast color averaging wizardy from https://stackoverflow.com/questions/8440631/how-would-you-average-two-32-bit-colors-packed-into-an-integer */ +#define AVERAGE(a, b) ( ((((a) ^ (b)) & 0xfefefefe) >> 1) + ((a) & (b)) ) + +static cc_bool Modern_IsOccluded(int x, int y, int z) { + BlockID block = World_SafeGetBlock(x, y, z); + if (Blocks.Brightness[block] > 0) { return false; } + /* If the block we're pulling colors from is solid, return a darker version of original and increment how many are like this */ + if (Blocks.FullOpaque[block] || (Blocks.Draw[block] == DRAW_TRANSPARENT && Blocks.BlocksLight[block] && Blocks.LightOffset[block] == 0xFF)) { + return true; + } + return false; +} + +static cc_bool Modern_CanStretch(BlockID initial, int chunkIndex, int x, int y, int z, Face face) { + return false; +} + +static int Modern_StretchXLiquid(int countIndex, int x, int y, int z, int chunkIndex, BlockID block) { + int count = 1; + if (Builder_OccludedLiquid(chunkIndex)) return 0; + AddVertices(block, FACE_YMAX); + return count; +} + +static int Modern_StretchX(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face) { + int count = 1; + AddVertices(block, face); + return count; +} + +static int Modern_StretchZ(int countIndex, int x, int y, int z, int chunkIndex, BlockID block, Face face) { + int count = 1; + AddVertices(block, face); + return count; +} + +static PackedCol Modern_GetColorX(PackedCol orig, int x, int y, int z, int oY, int oZ) { + cc_bool xOccluded = Modern_IsOccluded(x, y + oY, z ); + cc_bool zOccluded = Modern_IsOccluded(x, y , z + oZ); + cc_bool xzOccluded = Modern_IsOccluded(x, y + oY, z + oZ); + + PackedCol CoX = xOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_XSide_Fast(x, y + oY, z ); + PackedCol CoZ = zOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_XSide_Fast(x, y , z + oZ); + PackedCol CoXoZ = (xzOccluded || (xOccluded && zOccluded)) ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_XSide_Fast(x, y + oY, z + oZ); + + PackedCol ab = AVERAGE(CoX, CoZ); + PackedCol cd = AVERAGE(CoXoZ, orig); + return AVERAGE(ab, cd); +} +static void Modern_DrawXMin(int count, int x, int y, int z) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_XMIN); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.z, u2 = (count - 1) + adv_maxBB.z * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + PackedCol tint, white = PACKEDCOL_WHITE; + int offset = 1;// (Blocks.LightOffset[Builder_Block] >> FACE_XMIN) & 1; + PackedCol orig = Lighting.Color_XSide_Fast(x-offset, y, z); + PackedCol col0_0 = Builder_FullBright ? white : Modern_GetColorX(orig, x-offset, y, z, -1, -1); + PackedCol col1_0 = Builder_FullBright ? white : Modern_GetColorX(orig, x-offset, y, z, 1, -1); + PackedCol col1_1 = Builder_FullBright ? white : Modern_GetColorX(orig, x-offset, y, z, 1, 1); + PackedCol col0_1 = Builder_FullBright ? white : Modern_GetColorX(orig, x-offset, y, z, -1, 1); + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_XMIN]; + v.x = adv_x1; + v.y = adv_y2; v.z = adv_z2 + (count - 1); v.U = u2; v.V = v1; v.Col = col1_1; *vertices++ = v; + v.z = adv_z1; v.U = u1; v.Col = col1_0; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.z = adv_z2 + (count - 1); v.U = u2; v.Col = col0_1; *vertices++ = v; + part->faces.vertices[FACE_XMIN] = vertices; +} + +static void Modern_DrawXMax(int count, int x, int y, int z) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_XMAX); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = (count - adv_minBB.z), u2 = (1 - adv_maxBB.z) * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + PackedCol tint, white = PACKEDCOL_WHITE; + int offset = 1;// (Blocks.LightOffset[Builder_Block] >> FACE_XMAX) & 1; + PackedCol orig = Lighting.Color_XSide_Fast(x+offset, y, z); + PackedCol col0_0 = Builder_FullBright ? white : Modern_GetColorX(orig, x+offset, y, z, -1, -1); + PackedCol col1_0 = Builder_FullBright ? white : Modern_GetColorX(orig, x+offset, y, z, 1, -1); + PackedCol col1_1 = Builder_FullBright ? white : Modern_GetColorX(orig, x+offset, y, z, 1, 1); + PackedCol col0_1 = Builder_FullBright ? white : Modern_GetColorX(orig, x+offset, y, z, -1, 1); + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_XMAX]; + v.x = adv_x2; + v.y = adv_y2; v.z = adv_z2 + (count - 1); v.U = u2; v.V = v1; v.Col = col1_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.z = adv_z1; v.U = u1; v.Col = col0_0; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col1_0; *vertices++ = v; + part->faces.vertices[FACE_XMAX] = vertices; +} + +static PackedCol Modern_GetColorZ(PackedCol orig, int x, int y, int z, int oX, int oY) { + cc_bool xOccluded = Modern_IsOccluded(x + oX, y , z); + cc_bool zOccluded = Modern_IsOccluded(x, y + oY, z); + cc_bool xzOccluded = Modern_IsOccluded(x + oX, y + oY, z); + + PackedCol CoX = xOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_ZSide_Fast(x + oX, y , z); + PackedCol CoZ = zOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_ZSide_Fast(x , y + oY, z); + PackedCol CoXoZ = (xzOccluded || (xOccluded && zOccluded)) ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_ZSide_Fast(x + oX, y + oY, z); + + PackedCol ab = AVERAGE(CoX, CoZ); + PackedCol cd = AVERAGE(CoXoZ, orig); + return AVERAGE(ab, cd); +} +static void Modern_DrawZMin(int count, int x, int y, int z) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_ZMIN); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = (count - adv_minBB.x), u2 = (1 - adv_maxBB.x) * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + PackedCol tint, white = PACKEDCOL_WHITE; + int offset = 1;// (Blocks.LightOffset[Builder_Block] >> FACE_ZMIN) & 1; + PackedCol orig = Lighting.Color_ZSide_Fast(x, y, z-offset); + PackedCol col0_0 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z-offset, -1, -1); + PackedCol col1_0 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z-offset, 1, -1); + PackedCol col1_1 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z-offset, 1, 1); + PackedCol col0_1 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z-offset, -1, 1); + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_ZMIN]; + v.z = adv_z1; + v.x = adv_x1; v.y = adv_y1; v.U = u1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.y = adv_y2; v.V = v1; v.Col = col0_1; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col1_0; *vertices++ = v; + part->faces.vertices[FACE_ZMIN] = vertices; +} + +static void Modern_DrawZMax(int count, int x, int y, int z) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_ZMAX); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.x, u2 = (count - 1) + adv_maxBB.x * UV2_Scale; + float v1 = vOrigin + adv_maxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_minBB.y * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + PackedCol tint, white = PACKEDCOL_WHITE; + int offset = 1;// (Blocks.LightOffset[Builder_Block] >> FACE_ZMAX) & 1; + PackedCol orig = Lighting.Color_ZSide_Fast(x, y, z+offset); + PackedCol col0_0 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z+offset, -1, -1); + PackedCol col1_0 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z+offset, 1, -1); + PackedCol col1_1 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z+offset, 1, 1); + PackedCol col0_1 = Builder_FullBright ? white : Modern_GetColorZ(orig, x, y, z+offset, -1, 1); + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_ZMAX]; + v.z = adv_z2; + v.x = adv_x2 + (count - 1); v.y = adv_y2; v.U = u2; v.V = v1; v.Col = col1_1; *vertices++ = v; + v.x = adv_x1; v.U = u1; v.Col = col0_1; *vertices++ = v; + v.y = adv_y1; v.V = v2; v.Col = col0_0; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_0; *vertices++ = v; + part->faces.vertices[FACE_ZMAX] = vertices; +} + +static PackedCol Modern_GetColorYMin(PackedCol orig, int x, int y, int z, int oX, int oZ) { + cc_bool xOccluded = Modern_IsOccluded(x + oX, y, z ); + cc_bool zOccluded = Modern_IsOccluded(x, y, z + oZ); + cc_bool xzOccluded = Modern_IsOccluded(x + oX, y, z + oZ); + + PackedCol CoX = xOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_YMin_Fast(x + oX, y, z ); + PackedCol CoZ = zOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_YMin_Fast(x , y, z + oZ); + PackedCol CoXoZ = (xzOccluded || (xOccluded && zOccluded)) ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color_YMin_Fast(x + oX, y, z + oZ); + + PackedCol ab = AVERAGE(CoX, CoZ); + PackedCol cd = AVERAGE(CoXoZ, orig); + return AVERAGE(ab, cd); +} +static void Modern_DrawYMin(int count, int x, int y, int z) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_YMIN); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.x, u2 = (count - 1) + adv_maxBB.x * UV2_Scale; + float v1 = vOrigin + adv_minBB.z * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_maxBB.z * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + PackedCol tint, white = PACKEDCOL_WHITE; + int offset = 1;// (Blocks.LightOffset[Builder_Block] >> FACE_YMIN) & 1; + PackedCol orig = Lighting.Color_YMin_Fast(x, y-offset, z); + PackedCol col0_0 = Builder_FullBright ? white : Modern_GetColorYMin(orig, x, y-offset, z, -1, -1); + PackedCol col1_0 = Builder_FullBright ? white : Modern_GetColorYMin(orig, x, y-offset, z, 1, -1); + PackedCol col1_1 = Builder_FullBright ? white : Modern_GetColorYMin(orig, x, y-offset, z, 1, 1); + PackedCol col0_1 = Builder_FullBright ? white : Modern_GetColorYMin(orig, x, y-offset, z, -1, 1); + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_YMIN]; + v.y = adv_y1; + v.x = adv_x1; v.z = adv_z2; v.U = u1; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.z = adv_z1; v.V = v1; v.Col = col0_0; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_0; *vertices++ = v; + v.z = adv_z2; v.V = v2; v.Col = col1_1; *vertices++ = v; + part->faces.vertices[FACE_YMIN] = vertices; +} + +static PackedCol Modern_GetColorYMax(PackedCol orig, int x, int y, int z, int oX, int oZ) { + cc_bool xOccluded = Modern_IsOccluded(x + oX, y, z ); + cc_bool zOccluded = Modern_IsOccluded(x, y, z + oZ); + cc_bool xzOccluded = Modern_IsOccluded(x + oX, y, z + oZ); + + PackedCol CoX = xOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color(x + oX, y, z ); + PackedCol CoZ = zOccluded ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color(x , y, z + oZ); + PackedCol CoXoZ = (xzOccluded || (xOccluded && zOccluded)) ? PackedCol_Scale(orig, FANCY_AO) : Lighting.Color(x + oX, y, z + oZ); + + PackedCol ab = AVERAGE(CoX, CoZ); + PackedCol cd = AVERAGE(CoXoZ, orig); + return AVERAGE(ab, cd); +} +static void Modern_DrawYMax(int count, int x, int y, int z) { + TextureLoc texLoc = Block_Tex(Builder_Block, FACE_YMAX); + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = adv_minBB.x, u2 = (count - 1) + adv_maxBB.x * UV2_Scale; + float v1 = vOrigin + adv_minBB.z * Atlas1D.InvTileSize; + float v2 = vOrigin + adv_maxBB.z * Atlas1D.InvTileSize * UV2_Scale; + struct Builder1DPart* part = &Builder_Parts[adv_baseOffset + Atlas1D_Index(texLoc)]; + + PackedCol tint, white = PACKEDCOL_WHITE; + int offset = 1;// (Blocks.LightOffset[Builder_Block] >> FACE_YMAX) & 1; + PackedCol orig = Lighting.Color(x, y+offset, z); + PackedCol col0_0 = Builder_FullBright ? white : Modern_GetColorYMax(orig, x, y+offset, z, -1, -1); + PackedCol col1_0 = Builder_FullBright ? white : Modern_GetColorYMax(orig, x, y+offset, z, 1, -1); + PackedCol col1_1 = Builder_FullBright ? white : Modern_GetColorYMax(orig, x, y+offset, z, 1, 1); + PackedCol col0_1 = Builder_FullBright ? white : Modern_GetColorYMax(orig, x, y+offset, z, -1, 1); + + struct VertexTextured* vertices, v; + + if (adv_tinted) { + tint = Blocks.FogCol[Builder_Block]; + col0_0 = PackedCol_Tint(col0_0, tint); col1_0 = PackedCol_Tint(col1_0, tint); + col1_1 = PackedCol_Tint(col1_1, tint); col0_1 = PackedCol_Tint(col0_1, tint); + } + + vertices = part->faces.vertices[FACE_YMAX]; + v.y = adv_y2; + v.x = adv_x1; v.z = adv_z1; v.U = u1; v.V = v1; v.Col = col0_0; *vertices++ = v; + v.z = adv_z2; v.V = v2; v.Col = col0_1; *vertices++ = v; + v.x = adv_x2 + (count - 1); v.U = u2; v.Col = col1_1; *vertices++ = v; + v.z = adv_z1; v.V = v1; v.Col = col1_0; *vertices++ = v; + part->faces.vertices[FACE_YMAX] = vertices; +} + +static void Modern_RenderBlock(int index, int x, int y, int z) { + Vec3 min, max; + int count_XMin, count_XMax, count_ZMin; + int count_ZMax, count_YMin, count_YMax; + + if (Blocks.Draw[Builder_Block] == DRAW_SPRITE) { + Builder_DrawSprite(x, y, z); return; + } + + count_XMin = Builder_Counts[index + FACE_XMIN]; + count_XMax = Builder_Counts[index + FACE_XMAX]; + count_ZMin = Builder_Counts[index + FACE_ZMIN]; + count_ZMax = Builder_Counts[index + FACE_ZMAX]; + count_YMin = Builder_Counts[index + FACE_YMIN]; + count_YMax = Builder_Counts[index + FACE_YMAX]; + + if (!count_XMin && !count_XMax && !count_ZMin && + !count_ZMax && !count_YMin && !count_YMax) return; + + Builder_FullBright = Blocks.Brightness[Builder_Block]; + adv_baseOffset = (Blocks.Draw[Builder_Block] == DRAW_TRANSLUCENT) * ATLAS1D_MAX_ATLASES; + adv_tinted = Blocks.Tinted[Builder_Block]; + + min = Blocks.RenderMinBB[Builder_Block]; max = Blocks.RenderMaxBB[Builder_Block]; + adv_x1 = x + min.x; adv_y1 = y + min.y; adv_z1 = z + min.z; + adv_x2 = x + max.x; adv_y2 = y + max.y; adv_z2 = z + max.z; + + adv_minBB = Blocks.MinBB[Builder_Block]; adv_maxBB = Blocks.MaxBB[Builder_Block]; + adv_minBB.y = 1.0f - adv_minBB.y; adv_maxBB.y = 1.0f - adv_maxBB.y; + + if (count_XMin) Modern_DrawXMin(count_XMin, x, y, z); + if (count_XMax) Modern_DrawXMax(count_XMax, x, y, z); + if (count_ZMin) Modern_DrawZMin(count_ZMin, x, y, z); + if (count_ZMax) Modern_DrawZMax(count_ZMax, x, y, z); + if (count_YMin) Modern_DrawYMin(count_YMin, x, y, z); + if (count_YMax) Modern_DrawYMax(count_YMax, x, y, z); +} + +static void Modern_PrePrepareChunk(void) { + DefaultPrePrepateChunk(); + adv_bitFlags = Builder_BitFlags; +} + +static void ModernBuilder_SetActive(void) { + Builder_SetDefault(); + Builder_StretchXLiquid = Modern_StretchXLiquid; + Builder_StretchX = Modern_StretchX; + Builder_StretchZ = Modern_StretchZ; + Builder_RenderBlock = Modern_RenderBlock; + Builder_PrePrepareChunk = Modern_PrePrepareChunk; +} +#else +static void ModernBuilder_SetActive(void) { NormalBuilder_SetActive(); } +#endif + +/*########################################################################################################################* +*---------------------------------------------------Builder interface-----------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Builder_SmoothLighting; +void Builder_ApplyActive(void) { + if (Builder_SmoothLighting) { + if (Lighting_Mode != LIGHTING_MODE_CLASSIC) { + ModernBuilder_SetActive(); + } + else { + AdvBuilder_SetActive(); + } + } else { + NormalBuilder_SetActive(); + } +} + +static void OnInit(void) { + Builder_Offsets[FACE_XMIN] = -1; + Builder_Offsets[FACE_XMAX] = 1; + Builder_Offsets[FACE_ZMIN] = -EXTCHUNK_SIZE; + Builder_Offsets[FACE_ZMAX] = EXTCHUNK_SIZE; + Builder_Offsets[FACE_YMIN] = -EXTCHUNK_SIZE_2; + Builder_Offsets[FACE_YMAX] = EXTCHUNK_SIZE_2; + + if (!Game_ClassicMode) Builder_SmoothLighting = Options_GetBool(OPT_SMOOTH_LIGHTING, false); + Builder_ApplyActive(); +} + +static void OnNewMapLoaded(void) { + Builder_SidesLevel = max(0, Env_SidesHeight); + Builder_EdgeLevel = max(0, Env.EdgeHeight); +} + +struct IGameComponent Builder_Component = { + OnInit, /* Init */ + NULL, /* Free */ + NULL, /* Reset */ + NULL, /* OnNewMap */ + OnNewMapLoaded /* OnNewMapLoaded */ +}; diff --git a/src/Builder.h b/src/Builder.h new file mode 100644 index 0000000..5610b18 --- /dev/null +++ b/src/Builder.h @@ -0,0 +1,24 @@ +#ifndef CC_BUILDER_H +#define CC_BUILDER_H +#include "Core.h" +/* +Converts a 16x16x16 chunk into a mesh of vertices + NormalMeshBuilder: + Implements a simple chunk mesh builder, where each block face is a single colour + (whatever lighting engine returns as light colour for given block face at given coordinates) + +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct ChunkInfo; +struct IGameComponent; +extern struct IGameComponent Builder_Component; + +extern int Builder_SidesLevel, Builder_EdgeLevel; +/* Whether smooth/advanced lighting mesh builder is used. */ +extern cc_bool Builder_SmoothLighting; + +/* Builds the mesh of vertices for the given chunk. */ +void Builder_MakeChunk(struct ChunkInfo* info); + +void Builder_ApplyActive(void); +#endif diff --git a/src/Camera.c b/src/Camera.c new file mode 100644 index 0000000..4956d0e --- /dev/null +++ b/src/Camera.c @@ -0,0 +1,355 @@ +#include "Camera.h" +#include "ExtMath.h" +#include "Game.h" +#include "Window.h" +#include "Graphics.h" +#include "Funcs.h" +#include "Gui.h" +#include "Entity.h" +#include "Input.h" +#include "Event.h" +#include "Options.h" +#include "Picking.h" +#include "Platform.h" + +struct _CameraData Camera; +static struct RayTracer cameraClipPos; +static Vec2 cam_rotOffset; +static cc_bool cam_isForwardThird; +static float cam_deltaX, cam_deltaY; + +static void Camera_OnRawMovement(float deltaX, float deltaY) { + cam_deltaX += deltaX; cam_deltaY += deltaY; +} + +void Camera_KeyLookUpdate(float delta) { + if (Gui.InputGrab) return; + /* divide by 25 to have reasonable sensitivity for default mouse sens */ + float amount = (Camera.Sensitivity / 25.0f) * (1000 * delta); + + if (InputBind_IsPressed(BIND_LOOK_UP)) cam_deltaY -= amount; + if (InputBind_IsPressed(BIND_LOOK_DOWN)) cam_deltaY += amount; + if (InputBind_IsPressed(BIND_LOOK_LEFT)) cam_deltaX -= amount; + if (InputBind_IsPressed(BIND_LOOK_RIGHT)) cam_deltaX += amount; +} + +/*########################################################################################################################* +*--------------------------------------------------Perspective camera-----------------------------------------------------* +*#########################################################################################################################*/ +static void PerspectiveCamera_GetProjection(struct Matrix* proj) { + float fovy = Camera.Fov * MATH_DEG2RAD; + float aspectRatio = (float)Game.Width / (float)Game.Height; + Gfx_CalcPerspectiveMatrix(proj, fovy, aspectRatio, (float)Game_ViewDistance); +} + +static void PerspectiveCamera_GetView(struct Matrix* mat) { + Vec3 pos = Camera.CurrentPos; + Vec2 rot = Camera.Active->GetOrientation(); + Matrix_LookRot(mat, pos, rot); + Matrix_MulBy(mat, &Camera.TiltM); +} + +static void PerspectiveCamera_GetPickedBlock(struct RayTracer* t) { + struct LocalPlayer* p = Entities.CurPlayer; + struct Entity* e = &p->Base; + + Vec3 dir = Vec3_GetDirVector(e->Yaw * MATH_DEG2RAD, e->Pitch * MATH_DEG2RAD + Camera.TiltPitch); + Vec3 eyePos = Entity_GetEyePosition(e); + Picking_CalcPickedBlock(&eyePos, &dir, p->ReachDistance, t); +} + +#define CAMERA_SENSI_FACTOR (0.0002f / 3.0f * MATH_RAD2DEG) + +static Vec2 PerspectiveCamera_GetMouseDelta(float delta) { + float sensitivity = CAMERA_SENSI_FACTOR * Camera.Sensitivity; + static float speedX, speedY, newSpeedX, newSpeedY, accelX, accelY; + Vec2 v; + + if (Camera.Smooth) { + accelX = (cam_deltaX - speedX) * 35 / Camera.Mass; + accelY = (cam_deltaY - speedY) * 35 / Camera.Mass; + newSpeedX = accelX * delta + speedX; + newSpeedY = accelY * delta + speedY; + + /* High acceleration means velocity overshoots the correct position on low FPS, */ + /* causing wiggling. If newSpeed has opposite sign of speed, set speed to 0 */ + if (newSpeedX * speedX < 0) speedX = 0; + else speedX = newSpeedX; + if (newSpeedY * speedY < 0) speedY = 0; + else speedY = newSpeedY; + } else { + speedX = cam_deltaX; + speedY = cam_deltaY; + } + + v.x = speedX * sensitivity; v.y = speedY * sensitivity; + if (Camera.Invert) v.y = -v.y; + return v; +} + +static void PerspectiveCamera_UpdateMouseRotation(struct LocalPlayer* p, float delta) { + struct Entity* e = &p->Base; + struct LocationUpdate update; + Vec2 rot = PerspectiveCamera_GetMouseDelta(delta); + + if (Input_IsAltPressed() && Camera.Active->isThirdPerson) { + cam_rotOffset.x += rot.x; cam_rotOffset.y += rot.y; + return; + } + + update.flags = LU_HAS_YAW | LU_HAS_PITCH; + update.yaw = e->next.yaw + rot.x; + update.pitch = e->next.pitch + rot.y; + update.pitch = Math_ClampAngle(update.pitch); + + /* Need to make sure we don't cross the vertical axes, because that gets weird. */ + if (update.pitch >= 90.0f && update.pitch <= 270.0f) { + update.pitch = e->next.pitch < 180.0f ? 90.0f : 270.0f; + } + e->VTABLE->SetLocation(e, &update); +} + +static void PerspectiveCamera_UpdateMouse(struct LocalPlayer* p, float delta) { + if (!Gui.InputGrab && Window_Main.Focused) Window_UpdateRawMouse(); + + PerspectiveCamera_UpdateMouseRotation(p, delta); + cam_deltaX = 0; cam_deltaY = 0; +} + +static void PerspectiveCamera_CalcViewBobbing(struct LocalPlayer* p, float t, float velTiltScale) { + struct Entity* e = &p->Base; + + struct Matrix tiltY, velX; + float vel, fall; + if (!Game_ViewBobbing) { + Camera.TiltM = Matrix_Identity; + Camera.TiltPitch = 0.0f; + return; + } + + Matrix_RotateZ(&Camera.TiltM, -p->Tilt.TiltX * e->Anim.BobStrength); + Matrix_RotateX(&tiltY, Math_AbsF(p->Tilt.TiltY) * 3.0f * e->Anim.BobStrength); + Matrix_MulBy(&Camera.TiltM, &tiltY); + + Camera.BobbingHor = (e->Anim.BobbingHor * 0.3f) * e->Anim.BobStrength; + Camera.BobbingVer = (e->Anim.BobbingVer * 0.6f) * e->Anim.BobStrength; + + /* When standing on the ground, velocity.y is -0.08 (-gravity) */ + /* So add 0.08 to counteract that, so that vel is 0 when standing on ground */ + vel = 0.08f + Math_Lerp(p->OldVelocity.y, e->Velocity.y, t); + fall = -vel * 0.05f * p->Tilt.VelTiltStrength / velTiltScale; + + Matrix_RotateX(&velX, fall); + Matrix_MulBy(&Camera.TiltM, &velX); + if (!Game_ClassicMode) Camera.TiltPitch = fall; +} + + +/*########################################################################################################################* +*---------------------------------------------------First person camera---------------------------------------------------* +*#########################################################################################################################*/ +static Vec2 FirstPersonCamera_GetOrientation(void) { + struct LocalPlayer* p = Entities.CurPlayer; + struct Entity* e = &p->Base; + + Vec2 v; + v.x = e->Yaw * MATH_DEG2RAD; + v.y = e->Pitch * MATH_DEG2RAD; + return v; +} + +static Vec3 FirstPersonCamera_GetPosition(float t) { + struct LocalPlayer* p = Entities.CurPlayer; + struct Entity* e = &p->Base; + + Vec3 camPos = Entity_GetEyePosition(e); + float yaw = e->Yaw * MATH_DEG2RAD; + PerspectiveCamera_CalcViewBobbing(p, t, 1); + + camPos.y += Camera.BobbingVer; + camPos.x += Camera.BobbingHor * Math_CosF(yaw); + camPos.z += Camera.BobbingHor * Math_SinF(yaw); + return camPos; +} + +static cc_bool FirstPersonCamera_Zoom(float amount) { return false; } +static struct Camera cam_FirstPerson = { + false, + PerspectiveCamera_GetProjection, PerspectiveCamera_GetView, + FirstPersonCamera_GetOrientation, FirstPersonCamera_GetPosition, + PerspectiveCamera_UpdateMouse, Camera_OnRawMovement, + Window_EnableRawMouse, Window_DisableRawMouse, + PerspectiveCamera_GetPickedBlock, FirstPersonCamera_Zoom, +}; + + +/*########################################################################################################################* +*---------------------------------------------------Third person camera---------------------------------------------------* +*#########################################################################################################################*/ +#define DEF_ZOOM 3.0f +static float dist_third = DEF_ZOOM, dist_forward = DEF_ZOOM; + +static Vec2 ThirdPersonCamera_GetOrientation(void) { + struct LocalPlayer* p = Entities.CurPlayer; + struct Entity* e = &p->Base; + + Vec2 v; + v.x = e->Yaw * MATH_DEG2RAD; + v.y = e->Pitch * MATH_DEG2RAD; + if (cam_isForwardThird) { v.x += MATH_PI; v.y = -v.y; } + + v.x += cam_rotOffset.x * MATH_DEG2RAD; + v.y += cam_rotOffset.y * MATH_DEG2RAD; + return v; +} + +static float ThirdPersonCamera_GetZoom(struct LocalPlayer* p) { + float dist = cam_isForwardThird ? dist_forward : dist_third; + /* Don't allow zooming out when -fly */ + if (dist > DEF_ZOOM && !LocalPlayer_CheckCanZoom(p)) dist = DEF_ZOOM; + return dist; +} + +static Vec3 ThirdPersonCamera_GetPosition(float t) { + struct LocalPlayer* p = Entities.CurPlayer; + struct Entity* e = &p->Base; + + float dist = ThirdPersonCamera_GetZoom(p); + Vec3 target, dir; + Vec2 rot; + + PerspectiveCamera_CalcViewBobbing(p, t, dist); + target = Entity_GetEyePosition(e); + target.y += Camera.BobbingVer; + + rot = Camera.Active->GetOrientation(); + dir = Vec3_GetDirVector(rot.x, rot.y); + Vec3_Negate(&dir, &dir); + + Picking_ClipCameraPos(&target, &dir, dist, &cameraClipPos); + return cameraClipPos.intersect; +} + +static cc_bool ThirdPersonCamera_Zoom(float amount) { + float* dist = cam_isForwardThird ? &dist_forward : &dist_third; + float newDist = *dist - amount; + + *dist = max(newDist, 2.0f); + return true; +} + +static struct Camera cam_ThirdPerson = { + true, + PerspectiveCamera_GetProjection, PerspectiveCamera_GetView, + ThirdPersonCamera_GetOrientation, ThirdPersonCamera_GetPosition, + PerspectiveCamera_UpdateMouse, Camera_OnRawMovement, + Window_EnableRawMouse, Window_DisableRawMouse, + PerspectiveCamera_GetPickedBlock, ThirdPersonCamera_Zoom, +}; +static struct Camera cam_ForwardThird = { + true, + PerspectiveCamera_GetProjection, PerspectiveCamera_GetView, + ThirdPersonCamera_GetOrientation, ThirdPersonCamera_GetPosition, + PerspectiveCamera_UpdateMouse, Camera_OnRawMovement, + Window_EnableRawMouse, Window_DisableRawMouse, + PerspectiveCamera_GetPickedBlock, ThirdPersonCamera_Zoom, +}; + + +/*########################################################################################################################* +*-----------------------------------------------------General camera------------------------------------------------------* +*#########################################################################################################################*/ +static void OnRawMovement(void* obj, float deltaX, float deltaY) { + Camera.Active->OnRawMovement(deltaX, deltaY); +} + +static void OnAxisUpdate(void* obj, int port, int axis, float x, float y) { + if (!Input.RawMode) return; + if (Gamepad_AxisBehaviour[axis] != AXIS_BEHAVIOUR_CAMERA) return; + + Camera.Active->OnRawMovement(x, y); +} + +static void OnHacksChanged(void* obj) { + struct HacksComp* h = &Entities.CurPlayer->Hacks; + /* Leave third person if not allowed anymore */ + if (!h->CanUseThirdPerson || !h->Enabled) Camera_CycleActive(); +} + +void Camera_CycleActive(void) { + struct LocalPlayer* p = &LocalPlayer_Instances[0]; + if (Game_ClassicMode) return; + Camera.Active = Camera.Active->next; + + if (!p->Hacks.CanUseThirdPerson || !p->Hacks.Enabled) { + Camera.Active = &cam_FirstPerson; + } + cam_isForwardThird = Camera.Active == &cam_ForwardThird; + + /* reset rotation offset when changing cameras */ + cam_rotOffset.x = 0.0f; cam_rotOffset.y = 0.0f; + Camera_UpdateProjection(); +} + +static struct Camera* cams_head; +static struct Camera* cams_tail; +void Camera_Register(struct Camera* cam) { + LinkedList_Append(cam, cams_head, cams_tail); + /* want a circular linked list */ + cam->next = cams_head; +} + +static cc_bool cam_focussed; +void Camera_CheckFocus(void) { + cc_bool focus = Gui.InputGrab == NULL; + if (focus == cam_focussed) return; + cam_focussed = focus; + + if (focus) { + Camera.Active->AcquireFocus(); + } else { + Camera.Active->LoseFocus(); + } +} + +void Camera_SetFov(int fov) { + if (Camera.Fov == fov) return; + Camera.Fov = fov; + Camera_UpdateProjection(); +} + +void Camera_UpdateProjection(void) { + Camera.Active->GetProjection(&Gfx.Projection); + Gfx_LoadMatrix(MATRIX_PROJECTION, &Gfx.Projection); + Event_RaiseVoid(&GfxEvents.ProjectionChanged); +} + +static void OnInit(void) { + Camera_Register(&cam_FirstPerson); + Camera_Register(&cam_ThirdPerson); + Camera_Register(&cam_ForwardThird); + + Camera.Active = &cam_FirstPerson; + Event_Register_(&PointerEvents.RawMoved, NULL, OnRawMovement); + Event_Register_(&ControllerEvents.AxisUpdate, NULL, OnAxisUpdate); + Event_Register_(&UserEvents.HackPermsChanged, NULL, OnHacksChanged); + +#ifdef CC_BUILD_WIN + Camera.Sensitivity = Options_GetInt(OPT_SENSITIVITY, 1, 200, 40); +#else + Camera.Sensitivity = Options_GetInt(OPT_SENSITIVITY, 1, 200, 30); +#endif + Camera.Clipping = Options_GetBool(OPT_CAMERA_CLIPPING, true); + Camera.Invert = Options_GetBool(OPT_INVERT_MOUSE, false); + Camera.Mass = Options_GetFloat(OPT_CAMERA_MASS, 1, 100, 20); + Camera.Smooth = Options_GetBool(OPT_CAMERA_SMOOTH, false); + + Camera.DefaultFov = Options_GetInt(OPT_FIELD_OF_VIEW, 1, 179, 70); + Camera.Fov = Camera.DefaultFov; + Camera.ZoomFov = Camera.DefaultFov; + Camera_UpdateProjection(); +} + +struct IGameComponent Camera_Component = { + OnInit /* Init */ +}; diff --git a/src/Camera.h b/src/Camera.h new file mode 100644 index 0000000..f1a04cc --- /dev/null +++ b/src/Camera.h @@ -0,0 +1,85 @@ +#ifndef CC_CAMERA_H +#define CC_CAMERA_H +#include "Vectors.h" +/* +Represents a camera, may be first or third person +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct RayTracer; +struct Camera; +struct IGameComponent; +struct LocalPlayer; +extern struct IGameComponent Camera_Component; + +/* Shared data for cameras. */ +CC_VAR extern struct _CameraData { + /* How sensitive camera is to movements of mouse. */ + int Sensitivity; + /* Whether smooth/cinematic camera mode is used. */ + cc_bool Smooth; + /* Whether third person camera clip against blocks. */ + cc_bool Clipping; + /* Whether to invert vertical mouse movement. */ + cc_bool Invert; + + /* Tilt effect applied to the camera. */ + struct Matrix TiltM; + /* Bobbing offset of camera from player's eye. */ + float BobbingVer, BobbingHor; + + /* Cached position the camera is at. */ + Vec3 CurrentPos; + /* Camera user is currently using. */ + struct Camera* Active; + /* The mass (i.e. smoothness) of the smooth camera. */ + float Mass; + /* Field of view of the camera */ + int Fov, DefaultFov, ZoomFov; + + float TiltPitch; +} Camera; + +struct Camera { + /* Whether this camera is third person. (i.e. not allowed when -thirdperson in MOTD) */ + cc_bool isThirdPerson; + + /* Calculates the current projection matrix of this camera. */ + void (*GetProjection)(struct Matrix* proj); + /* Calculates the current modelview matrix of this camera. */ + void (*GetView)(struct Matrix* view); + + /* Returns the current orientation of the camera. */ + Vec2 (*GetOrientation)(void); + /* Returns the current interpolated position of the camera. */ + Vec3 (*GetPosition)(float t); + + /* Called to update the camera's state. */ + /* Typically, this is used to adjust yaw/pitch based on accumulated mouse movement. */ + void (*UpdateMouse)(struct LocalPlayer* p, float delta); + /* Called when mouse/pointer has moved. */ + void (*OnRawMovement)(float deltaX, float deltaY); + /* Called when user closes all menus, and is interacting with camera again. */ + void (*AcquireFocus)(void); + /* Called when user is no longer interacting with camera. (e.g. opened menu) */ + void (*LoseFocus)(void); + + /* Calculates selected block in the world, based on camera's current state */ + void (*GetPickedBlock)(struct RayTracer* t); + /* Zooms the camera in or out when scrolling mouse wheel. */ + cc_bool (*Zoom)(float amount); + + /* Next camera in linked list of cameras. */ + struct Camera* next; +}; + +/* Switches to next camera in the list of cameras. */ +void Camera_CycleActive(void); +/* Registers a camera for use. */ +CC_API void Camera_Register(struct Camera* camera); +/* Checks whether camera is still focused or not. */ +/* If focus changes, calls AcquireFocus or LoseFocus */ +void Camera_CheckFocus(void); +void Camera_UpdateProjection(void); +void Camera_SetFov(int fov); +void Camera_KeyLookUpdate(float delta); +#endif diff --git a/src/Chat.c b/src/Chat.c new file mode 100644 index 0000000..5f45d78 --- /dev/null +++ b/src/Chat.c @@ -0,0 +1,311 @@ +#include "Chat.h" +#include "Commands.h" +#include "String.h" +#include "Stream.h" +#include "Event.h" +#include "Game.h" +#include "Logger.h" +#include "Server.h" +#include "Funcs.h" +#include "Utils.h" +#include "Options.h" +#include "Drawer2D.h" + +static char status[5][STRING_SIZE]; +static char bottom[3][STRING_SIZE]; +static char client[2][STRING_SIZE]; +static char announcement[STRING_SIZE]; +static char bigAnnouncement[STRING_SIZE]; +static char smallAnnouncement[STRING_SIZE]; + +cc_string Chat_Status[5] = { String_FromArray(status[0]), String_FromArray(status[1]), String_FromArray(status[2]), + String_FromArray(status[3]), String_FromArray(status[4]) }; +cc_string Chat_BottomRight[3] = { String_FromArray(bottom[0]), String_FromArray(bottom[1]), String_FromArray(bottom[2]) }; +cc_string Chat_ClientStatus[2] = { String_FromArray(client[0]), String_FromArray(client[1]) }; + +cc_string Chat_Announcement = String_FromArray(announcement); +cc_string Chat_BigAnnouncement = String_FromArray(bigAnnouncement); +cc_string Chat_SmallAnnouncement = String_FromArray(smallAnnouncement); + +double Chat_AnnouncementReceived; +double Chat_BigAnnouncementReceived; +double Chat_SmallAnnouncementReceived; + +struct StringsBuffer Chat_Log, Chat_InputLog; +cc_bool Chat_Logging; + +/*########################################################################################################################* +*-------------------------------------------------------Chat logging------------------------------------------------------* +*#########################################################################################################################*/ +double Chat_RecentLogTimes[CHATLOG_TIME_MASK + 1]; + +static void ClearChatLogs(void) { + Mem_Set(Chat_RecentLogTimes, 0, sizeof(Chat_RecentLogTimes)); + StringsBuffer_Clear(&Chat_Log); +} + +static char logNameBuffer[STRING_SIZE]; +static cc_string logName = String_FromArray(logNameBuffer); +static char logPathBuffer[FILENAME_SIZE]; +static cc_string logPath = String_FromArray(logPathBuffer); + +static struct Stream logStream; +static int lastLogDay, lastLogMonth, lastLogYear; + +/* Resets log name to empty and resets last log date */ +static void ResetLogFile(void) { + logName.length = 0; + lastLogYear = -123; +} + +/* Closes handle to the chat log file */ +static void CloseLogFile(void) { + cc_result res; + if (!logStream.meta.file) return; + + res = logStream.Close(&logStream); + if (res) { Logger_SysWarn2(res, "closing", &logPath); } +} + +/* Whether the given character is an allowed in a log filename */ +static cc_bool AllowedLogNameChar(char c) { + return + c == '{' || c == '}' || c == '[' || c == ']' || c == '(' || c == ')' || + (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +void Chat_SetLogName(const cc_string* name) { + char c; + int i; + if (logName.length) return; + + for (i = 0; i < name->length; i++) { + c = name->buffer[i]; + + if (AllowedLogNameChar(c)) { + String_Append(&logName, c); + } else if (c == '&') { + i++; /* skip over following color code */ + } + } +} + +void Chat_DisableLogging(void) { + Chat_Logging = false; + lastLogYear = -321; + Chat_AddRaw("&cDisabling chat logging"); + CloseLogFile(); +} + +static cc_bool CreateLogsDirectory(void) { + static const cc_string dir = String_FromConst("logs"); + cc_result res; + /* Utils_EnsureDirectory cannot be used here because it causes a stack overflow */ + /* when running the game and an error occurs when trying to create the directory */ + /* This happens because when running the game, Logger_WarnFunc is changed to log */ + /* a message in chat instead of showing a dialog box, which causes the following */ + /* functions to be called in a recursive loop: */ + /* */ + /* Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog -> OpenChatLog */ + /* --> Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog -> OpenChatLog */ + /* --> Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog -> OpenChatLog */ + /* --> Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog ... */ + /* and so on, until eventually the stack overflows */ + res = Directory_Create(&dir); + if (!res || res == ReturnCode_DirectoryExists) return true; + + Chat_DisableLogging(); + Logger_SysWarn2(res, "creating directory", &dir); + return false; +} + +static void OpenChatLog(struct DateTime* now) { + cc_result res; + int i; + if (Platform_ReadonlyFilesystem || !CreateLogsDirectory()) return; + + /* Ensure multiple instances do not end up overwriting each other's log entries. */ + for (i = 0; i < 20; i++) { + logPath.length = 0; + String_Format3(&logPath, "logs/%p4-%p2-%p2 ", &now->year, &now->month, &now->day); + + if (i > 0) { + String_Format2(&logPath, "%s _%i.txt", &logName, &i); + } else { + String_Format1(&logPath, "%s.txt", &logName); + } + + res = Stream_AppendFile(&logStream, &logPath); + if (res && res != ReturnCode_FileShareViolation) { + Chat_DisableLogging(); + Logger_SysWarn2(res, "appending to", &logPath); + return; + } + + if (res == ReturnCode_FileShareViolation) continue; + return; + } + + Chat_DisableLogging(); + Chat_Add1("&cFailed to open a chat log file after %i tries, giving up", &i); +} + +static void AppendChatLog(const cc_string* text) { + cc_string str; char strBuffer[DRAWER2D_MAX_TEXT_LENGTH]; + struct DateTime now; + cc_result res; + + if (!logName.length || !Chat_Logging) return; + DateTime_CurrentLocal(&now); + + if (now.day != lastLogDay || now.month != lastLogMonth || now.year != lastLogYear) { + CloseLogFile(); + OpenChatLog(&now); + } + + lastLogDay = now.day; lastLogMonth = now.month; lastLogYear = now.year; + if (!logStream.meta.file) return; + + /* [HH:mm:ss] text */ + String_InitArray(str, strBuffer); + String_Format3(&str, "[%p2:%p2:%p2] ", &now.hour, &now.minute, &now.second); + Drawer2D_WithoutColors(&str, text); + + res = Stream_WriteLine(&logStream, &str); + if (!res) return; + Chat_DisableLogging(); + Logger_SysWarn2(res, "writing to", &logPath); +} + +void Chat_Add1(const char* format, const void* a1) { + Chat_Add4(format, a1, NULL, NULL, NULL); +} +void Chat_Add2(const char* format, const void* a1, const void* a2) { + Chat_Add4(format, a1, a2, NULL, NULL); +} +void Chat_Add3(const char* format, const void* a1, const void* a2, const void* a3) { + Chat_Add4(format, a1, a2, a3, NULL); +} +void Chat_Add4(const char* format, const void* a1, const void* a2, const void* a3, const void* a4) { + cc_string msg; char msgBuffer[STRING_SIZE * 2]; + String_InitArray(msg, msgBuffer); + + String_Format4(&msg, format, a1, a2, a3, a4); + Chat_AddOf(&msg, MSG_TYPE_NORMAL); +} + +void Chat_AddRaw(const char* raw) { + cc_string str = String_FromReadonly(raw); + Chat_AddOf(&str, MSG_TYPE_NORMAL); +} +void Chat_Add(const cc_string* text) { Chat_AddOf(text, MSG_TYPE_NORMAL); } + +void Chat_AddOf(const cc_string* text, int msgType) { + cc_string str; + if (msgType == MSG_TYPE_NORMAL) { + /* Check for chat overflow (see issue #837) */ + /* This happens because Offset/Length are packed into a single 32 bit value, */ + /* with 9 bits used for length. Hence if offset exceeds 2^23 (8388608), it */ + /* overflows and earlier chat messages start wrongly appearing instead */ + if (Chat_Log.totalLength > 8388000) { + ClearChatLogs(); + Chat_AddRaw("&cChat log cleared as it hit 8.3 million character limit"); + } + + /* StringsBuffer_Add will abort game if try to add string > 511 characters */ + str = *text; + str.length = min(str.length, DRAWER2D_MAX_TEXT_LENGTH); + + Chat_GetLogTime(Chat_Log.count) = Game.Time; + AppendChatLog(&str); + StringsBuffer_Add(&Chat_Log, &str); + } else if (msgType >= MSG_TYPE_STATUS_1 && msgType <= MSG_TYPE_STATUS_3) { + /* Status[0] is for texture pack downloading message */ + /* Status[1] is for reduced performance mode message */ + String_Copy(&Chat_Status[2 + (msgType - MSG_TYPE_STATUS_1)], text); + } else if (msgType >= MSG_TYPE_BOTTOMRIGHT_1 && msgType <= MSG_TYPE_BOTTOMRIGHT_3) { + String_Copy(&Chat_BottomRight[msgType - MSG_TYPE_BOTTOMRIGHT_1], text); + } else if (msgType == MSG_TYPE_ANNOUNCEMENT) { + String_Copy(&Chat_Announcement, text); + Chat_AnnouncementReceived = Game.Time; + } else if (msgType == MSG_TYPE_BIGANNOUNCEMENT) { + String_Copy(&Chat_BigAnnouncement, text); + Chat_BigAnnouncementReceived = Game.Time; + } else if (msgType == MSG_TYPE_SMALLANNOUNCEMENT) { + String_Copy(&Chat_SmallAnnouncement, text); + Chat_SmallAnnouncementReceived = Game.Time; + } else if (msgType >= MSG_TYPE_CLIENTSTATUS_1 && msgType <= MSG_TYPE_CLIENTSTATUS_2) { + String_Copy(&Chat_ClientStatus[msgType - MSG_TYPE_CLIENTSTATUS_1], text); + } else if (msgType >= MSG_TYPE_EXTRASTATUS_1 && msgType <= MSG_TYPE_EXTRASTATUS_2) { + String_Copy(&Chat_Status[msgType - MSG_TYPE_EXTRASTATUS_1], text); + } + + Event_RaiseChat(&ChatEvents.ChatReceived, text, msgType); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Generic chat------------------------------------------------------* +*#########################################################################################################################*/ +static void LogInputUsage(const cc_string* text) { + /* Simplify navigating through input history by not logging duplicate entries */ + if (Chat_InputLog.count) { + int lastIndex = Chat_InputLog.count - 1; + cc_string last = StringsBuffer_UNSAFE_Get(&Chat_InputLog, lastIndex); + + if (String_Equals(text, &last)) return; + } + StringsBuffer_Add(&Chat_InputLog, text); +} + +void Chat_Send(const cc_string* text, cc_bool logUsage) { + if (!text->length) return; + Event_RaiseChat(&ChatEvents.ChatSending, text, 0); + if (logUsage) LogInputUsage(text); + + if (!Commands_Execute(text)) { + Server.SendChat(text); + } +} + +static void OnInit(void) { +#if defined CC_BUILD_MOBILE || defined CC_BUILD_WEB + /* Better to not log chat by default on mobile/web, */ + /* since it's not easily visible to end users */ + Chat_Logging = Options_GetBool(OPT_CHAT_LOGGING, false); +#else + Chat_Logging = Options_GetBool(OPT_CHAT_LOGGING, true); +#endif +} + +static void ClearCPEMessages(void) { + Chat_AddOf(&String_Empty, MSG_TYPE_ANNOUNCEMENT); + Chat_AddOf(&String_Empty, MSG_TYPE_BIGANNOUNCEMENT); + Chat_AddOf(&String_Empty, MSG_TYPE_SMALLANNOUNCEMENT); + Chat_AddOf(&String_Empty, MSG_TYPE_STATUS_1); + Chat_AddOf(&String_Empty, MSG_TYPE_STATUS_2); + Chat_AddOf(&String_Empty, MSG_TYPE_STATUS_3); + Chat_AddOf(&String_Empty, MSG_TYPE_BOTTOMRIGHT_1); + Chat_AddOf(&String_Empty, MSG_TYPE_BOTTOMRIGHT_2); + Chat_AddOf(&String_Empty, MSG_TYPE_BOTTOMRIGHT_3); +} + +static void OnReset(void) { + CloseLogFile(); + ResetLogFile(); + ClearCPEMessages(); +} + +static void OnFree(void) { + CloseLogFile(); + ClearCPEMessages(); + + ClearChatLogs(); + StringsBuffer_Clear(&Chat_InputLog); +} + +struct IGameComponent Chat_Component = { + OnInit, /* Init */ + OnFree, /* Free */ + OnReset /* Reset */ +}; diff --git a/src/Chat.h b/src/Chat.h new file mode 100644 index 0000000..8fd176f --- /dev/null +++ b/src/Chat.h @@ -0,0 +1,74 @@ +#ifndef CC_CHAT_H +#define CC_CHAT_H +#include "Core.h" +/* Manages sending, adding, logging and handling chat. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +struct StringsBuffer; +extern struct IGameComponent Chat_Component; + +enum MsgType { + MSG_TYPE_NORMAL = 0, + MSG_TYPE_STATUS_1 = 1, + MSG_TYPE_STATUS_2 = 2, + MSG_TYPE_STATUS_3 = 3, + MSG_TYPE_BOTTOMRIGHT_1 = 11, + MSG_TYPE_BOTTOMRIGHT_2 = 12, + MSG_TYPE_BOTTOMRIGHT_3 = 13, + MSG_TYPE_ANNOUNCEMENT = 100, + MSG_TYPE_BIGANNOUNCEMENT = 101, + MSG_TYPE_SMALLANNOUNCEMENT = 102, + MSG_TYPE_CLIENTSTATUS_1 = 256, /* Cuboid messages */ + MSG_TYPE_CLIENTSTATUS_2 = 257, /* Tab list matching names */ + MSG_TYPE_EXTRASTATUS_1 = 360, + MSG_TYPE_EXTRASTATUS_2 = 361 +}; + +/* NOTE: this must be: (next power of next larger than GUI_MAX_CHATLINES) - 1 */ +#define CHATLOG_TIME_MASK 31 +/* Time most recent chat message were received at */ +extern double Chat_RecentLogTimes[CHATLOG_TIME_MASK + 1]; +#define Chat_GetLogTime(i) Chat_RecentLogTimes[(i) & CHATLOG_TIME_MASK] + +/* Times at which last announcement messages were received */ +extern double Chat_AnnouncementReceived; +extern double Chat_BigAnnouncementReceived; +extern double Chat_SmallAnnouncementReceived; + +extern cc_string Chat_Status[5], Chat_BottomRight[3], Chat_ClientStatus[2]; +extern cc_string Chat_Announcement, Chat_BigAnnouncement, Chat_SmallAnnouncement; +/* All chat messages received */ +extern struct StringsBuffer Chat_Log; +/* All chat input entered by the user */ +extern struct StringsBuffer Chat_InputLog; +/* Whether chat messages are logged to disc */ +extern cc_bool Chat_Logging; + +/* Sets the name of log file (no .txt, so e.g. just "singleplayer") */ +/* NOTE: This can only be set once. */ +void Chat_SetLogName(const cc_string* name); +/* Disables chat logging and closes currently open chat log file. */ +void Chat_DisableLogging(void); +/* Sends a chat message, raising ChatEvents.ChatSending event. */ +/* NOTE: If logUsage is true, can press 'up' in chat input menu later to retype this. */ +/* NOTE: /client is always interpreted as client-side commands. */ +/* In multiplayer this is sent to the server, in singleplayer just Chat_Add. */ +CC_API void Chat_Send( const cc_string* text, cc_bool logUsage); +typedef void (*FP_Chat_Send)(const cc_string* text, cc_bool logUsage); +/* Shorthand for Chat_AddOf(str, MSG_TYPE_NORMAL) */ +CC_API void Chat_Add( const cc_string* text); +typedef void (*FP_Chat_Add)(const cc_string* text); +/* Adds a chat message, raising ChatEvents.ChatReceived event. */ +/* MSG_TYPE_NORMAL is usually used for player chat and command messages. */ +/* Other message types are usually used for info/status messages. */ +CC_API void Chat_AddOf( const cc_string* text, int msgType); +typedef void (*FP_Chat_AddOf)(const cc_string* text, int msgType); +/* Shorthand for Chat_AddOf(String_FromReadonly(raw), MSG_TYPE_NORMAL) */ +void Chat_AddRaw(const char* raw); + +CC_API void Chat_Add1(const char* format, const void* a1); +CC_API void Chat_Add2(const char* format, const void* a1, const void* a2); +CC_API void Chat_Add3(const char* format, const void* a1, const void* a2, const void* a3); +CC_API void Chat_Add4(const char* format, const void* a1, const void* a2, const void* a3, const void* a4); +#endif diff --git a/src/Commands.c b/src/Commands.c new file mode 100644 index 0000000..3976d18 --- /dev/null +++ b/src/Commands.c @@ -0,0 +1,742 @@ +#include "Commands.h" +#include "Chat.h" +#include "String.h" +#include "Event.h" +#include "Game.h" +#include "Logger.h" +#include "Server.h" +#include "World.h" +#include "Inventory.h" +#include "Entity.h" +#include "Window.h" +#include "Graphics.h" +#include "Funcs.h" +#include "Block.h" +#include "EnvRenderer.h" +#include "Utils.h" +#include "TexturePack.h" +#include "Options.h" +#include "Drawer2D.h" + +#define COMMANDS_PREFIX "/client" +#define COMMANDS_PREFIX_SPACE "/client " +static struct ChatCommand* cmds_head; +static struct ChatCommand* cmds_tail; + +void Commands_Register(struct ChatCommand* cmd) { + LinkedList_Append(cmd, cmds_head, cmds_tail); +} + + +/*########################################################################################################################* +*------------------------------------------------------Command handling---------------------------------------------------* +*#########################################################################################################################*/ +static struct ChatCommand* Commands_FindMatch(const cc_string* cmdName) { + struct ChatCommand* match = NULL; + struct ChatCommand* cmd; + cc_string name; + + for (cmd = cmds_head; cmd; cmd = cmd->next) + { + name = String_FromReadonly(cmd->name); + if (String_CaselessEquals(&name, cmdName)) return cmd; + } + + for (cmd = cmds_head; cmd; cmd = cmd->next) + { + name = String_FromReadonly(cmd->name); + if (!String_CaselessStarts(&name, cmdName)) continue; + + if (match) { + Chat_Add1("&e/client: Multiple commands found that start with: \"&f%s&e\".", cmdName); + return NULL; + } + match = cmd; + } + + if (!match) { + Chat_Add1("&e/client: Unrecognised command: \"&f%s&e\".", cmdName); + Chat_AddRaw("&e/client: Type &a/client &efor a list of commands."); + } + return match; +} + +static void Commands_PrintDefault(void) { + cc_string str; char strBuffer[STRING_SIZE]; + struct ChatCommand* cmd; + cc_string name; + + Chat_AddRaw("&eList of client commands:"); + String_InitArray(str, strBuffer); + + for (cmd = cmds_head; cmd; cmd = cmd->next) { + name = String_FromReadonly(cmd->name); + + if ((str.length + name.length + 2) > str.capacity) { + Chat_Add(&str); + str.length = 0; + } + String_AppendString(&str, &name); + String_AppendConst(&str, ", "); + } + + if (str.length) { Chat_Add(&str); } + Chat_AddRaw("&eTo see help for a command, type /client help [cmd name]"); +} + +cc_bool Commands_Execute(const cc_string* input) { + static const cc_string prefixSpace = String_FromConst(COMMANDS_PREFIX_SPACE); + static const cc_string prefix = String_FromConst(COMMANDS_PREFIX); + cc_string text; + + struct ChatCommand* cmd; + int offset, count; + cc_string name, value; + cc_string args[50]; + + if (String_CaselessStarts(input, &prefixSpace)) { + /* /client [command] [args] */ + offset = prefixSpace.length; + } else if (String_CaselessEquals(input, &prefix)) { + /* /client */ + offset = prefix.length; + } else if (Server.IsSinglePlayer && String_CaselessStarts(input, &prefix)) { + /* /client[command] [args] */ + offset = prefix.length; + } else if (Server.IsSinglePlayer && input->length && input->buffer[0] == '/') { + /* /[command] [args] */ + offset = 1; + } else { + return false; + } + + text = String_UNSAFE_SubstringAt(input, offset); + /* Check for only / or /client */ + if (!text.length) { Commands_PrintDefault(); return true; } + + String_UNSAFE_Separate(&text, ' ', &name, &value); + cmd = Commands_FindMatch(&name); + if (!cmd) return true; + + if ((cmd->flags & COMMAND_FLAG_SINGLEPLAYER_ONLY) && !Server.IsSinglePlayer) { + Chat_Add1("&e/client: \"&f%s&e\" can only be used in singleplayer.", &name); + return true; + } + + if (cmd->flags & COMMAND_FLAG_UNSPLIT_ARGS) { + /* argsCount = 0 if value.length is 0, 1 otherwise */ + cmd->Execute(&value, value.length != 0); + } else { + count = String_UNSAFE_Split(&value, ' ', args, Array_Elems(args)); + cmd->Execute(args, value.length ? count : 0); + } + return true; +} + + +/*########################################################################################################################* +*------------------------------------------------------Simple commands----------------------------------------------------* +*#########################################################################################################################*/ +static void HelpCommand_Execute(const cc_string* args, int argsCount) { + struct ChatCommand* cmd; + int i; + + if (!argsCount) { Commands_PrintDefault(); return; } + cmd = Commands_FindMatch(args); + if (!cmd) return; + + for (i = 0; i < Array_Elems(cmd->help); i++) { + if (!cmd->help[i]) continue; + Chat_AddRaw(cmd->help[i]); + } +} + +static struct ChatCommand HelpCommand = { + "Help", HelpCommand_Execute, + COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client help [command name]", + "&eDisplays the help for the given command.", + } +}; + +static void GpuInfoCommand_Execute(const cc_string* args, int argsCount) { + char buffer[7 * STRING_SIZE]; + cc_string str, line; + String_InitArray(str, buffer); + Gfx_GetApiInfo(&str); + + while (str.length) { + String_UNSAFE_SplitBy(&str, '\n', &line); + if (line.length) Chat_Add1("&a%s", &line); + } +} + +static struct ChatCommand GpuInfoCommand = { + "GpuInfo", GpuInfoCommand_Execute, + COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client gpuinfo", + "&eDisplays information about your GPU.", + } +}; + +static void RenderTypeCommand_Execute(const cc_string* args, int argsCount) { + int flags; + if (!argsCount) { + Chat_AddRaw("&e/client: &cYou didn't specify a new render type."); return; + } + + flags = EnvRenderer_CalcFlags(args); + if (flags >= 0) { + EnvRenderer_SetMode(flags); + Options_Set(OPT_RENDER_TYPE, args); + Chat_Add1("&e/client: &fRender type is now %s.", args); + } else { + Chat_Add1("&e/client: &cUnrecognised render type &f\"%s\"&c.", args); + } +} + +static struct ChatCommand RenderTypeCommand = { + "RenderType", RenderTypeCommand_Execute, + COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client rendertype [normal/legacy/fast]", + "&bnormal: &eDefault render mode, with all environmental effects enabled", + "&blegacy: &eSame as normal mode, &cbut is usually slightly slower", + " &eIf you have issues with clouds and map edges disappearing randomly, use this mode", + "&bfast: &eSacrifices clouds, fog and overhead sky for faster performance", + } +}; + +static void ResolutionCommand_Execute(const cc_string* args, int argsCount) { + int width, height; + if (argsCount < 2) { + Chat_Add4("&e/client: &fCurrent resolution is %i@%f2 x %i@%f2", + &Window_Main.Width, &DisplayInfo.ScaleX, &Window_Main.Height, &DisplayInfo.ScaleY); + } else if (!Convert_ParseInt(&args[0], &width) || !Convert_ParseInt(&args[1], &height)) { + Chat_AddRaw("&e/client: &cWidth and height must be integers."); + } else if (width <= 0 || height <= 0) { + Chat_AddRaw("&e/client: &cWidth and height must be above 0."); + } else { + Window_SetSize(width, height); + /* Window_Create uses these, but scales by DPI. Hence DPI unscale them here. */ + Options_SetInt(OPT_WINDOW_WIDTH, (int)(width / DisplayInfo.ScaleX)); + Options_SetInt(OPT_WINDOW_HEIGHT, (int)(height / DisplayInfo.ScaleY)); + } +} + +static struct ChatCommand ResolutionCommand = { + "Resolution", ResolutionCommand_Execute, + 0, + { + "&a/client resolution [width] [height]", + "&ePrecisely sets the size of the rendered window.", + } +}; + +static void ModelCommand_Execute(const cc_string* args, int argsCount) { + if (argsCount) { + Entity_SetModel(&Entities.CurPlayer->Base, args); + } else { + Chat_AddRaw("&e/client model: &cYou didn't specify a model name."); + } +} + +static struct ChatCommand ModelCommand = { + "Model", ModelCommand_Execute, + COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client model [name]", + "&bnames: &echibi, chicken, creeper, human, pig, sheep", + "&e skeleton, spider, zombie, sit, ", + } +}; + +static void ClearDeniedCommand_Execute(const cc_string* args, int argsCount) { + int count = TextureCache_ClearDenied(); + Chat_Add1("Removed &e%i &fdenied texture pack URLs.", &count); +} + +static struct ChatCommand ClearDeniedCommand = { + "ClearDenied", ClearDeniedCommand_Execute, + COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client cleardenied", + "&eClears the list of texture pack URLs you have denied", + } +}; + +static void MotdCommand_Execute(const cc_string* args, int argsCount) { + if (Server.IsSinglePlayer) { + Chat_AddRaw("&eThis command can only be used in multiplayer."); + return; + } + Chat_Add1("&eName: &f%s", &Server.Name); + Chat_Add1("&eMOTD: &f%s", &Server.MOTD); +} + +static struct ChatCommand MotdCommand = { + "Motd", MotdCommand_Execute, + COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client motd", + "&eDisplays the server's name and MOTD." + } +}; + +/*########################################################################################################################* +*-------------------------------------------------------DrawOpCommand-----------------------------------------------------* +*#########################################################################################################################*/ +static IVec3 drawOp_mark1, drawOp_mark2; +static cc_bool drawOp_persist, drawOp_hooked, drawOp_hasMark1; +static const char* drawOp_name; +static void (*drawOp_Func)(IVec3 min, IVec3 max); + +static void DrawOpCommand_BlockChanged(void* obj, IVec3 coords, BlockID old, BlockID now); +static void DrawOpCommand_ResetState(void) { + if (drawOp_hooked) { + Event_Unregister_(&UserEvents.BlockChanged, NULL, DrawOpCommand_BlockChanged); + drawOp_hooked = false; + } + + drawOp_hasMark1 = false; +} + +static void DrawOpCommand_Begin(void) { + cc_string msg; char msgBuffer[STRING_SIZE]; + String_InitArray(msg, msgBuffer); + + String_Format1(&msg, "&e%c: &fPlace or delete a block.", drawOp_name); + Chat_AddOf(&msg, MSG_TYPE_CLIENTSTATUS_1); + + Event_Register_(&UserEvents.BlockChanged, NULL, DrawOpCommand_BlockChanged); + drawOp_hooked = true; +} + + +static void DrawOpCommand_Execute(void) { + IVec3 min, max; + + IVec3_Min(&min, &drawOp_mark1, &drawOp_mark2); + IVec3_Max(&max, &drawOp_mark1, &drawOp_mark2); + if (!World_Contains(min.x, min.y, min.z)) return; + if (!World_Contains(max.x, max.y, max.z)) return; + + drawOp_Func(min, max); +} + +static void DrawOpCommand_BlockChanged(void* obj, IVec3 coords, BlockID old, BlockID now) { + cc_string msg; char msgBuffer[STRING_SIZE]; + String_InitArray(msg, msgBuffer); + Game_UpdateBlock(coords.x, coords.y, coords.z, old); + + if (!drawOp_hasMark1) { + drawOp_mark1 = coords; + drawOp_hasMark1 = true; + + String_Format4(&msg, "&e%c: &fMark 1 placed at (%i, %i, %i), place mark 2.", + drawOp_name, &coords.x, &coords.y, &coords.z); + Chat_AddOf(&msg, MSG_TYPE_CLIENTSTATUS_1); + } else { + drawOp_mark2 = coords; + + DrawOpCommand_Execute(); + DrawOpCommand_ResetState(); + + if (!drawOp_persist) { + Chat_AddOf(&String_Empty, MSG_TYPE_CLIENTSTATUS_1); + } else { + DrawOpCommand_Begin(); + } + } +} + +static const cc_string yes_string = String_FromConst("yes"); +static void DrawOpCommand_ExtractPersistArg(cc_string* value) { + drawOp_persist = false; + if (!String_CaselessEnds(value, &yes_string)) return; + + drawOp_persist = true; + value->length -= 3; + String_UNSAFE_TrimEnd(value); +} + +static int DrawOpCommand_ParseBlock(const cc_string* arg) { + int block = Block_Parse(arg); + + if (block == -1) { + Chat_Add2("&e%c: &c\"%s\" is not a valid block name or id.", drawOp_name, arg); + return -1; + } + + if (block > Game_Version.MaxCoreBlock && !Block_IsCustomDefined(block)) { + Chat_Add2("&e%c: &cThere is no block with id \"%s\".", drawOp_name, arg); + return -1; + } + return block; +} + + +/*########################################################################################################################* +*-------------------------------------------------------CuboidCommand-----------------------------------------------------* +*#########################################################################################################################*/ +static int cuboid_block; + +static void CuboidCommand_Draw(IVec3 min, IVec3 max) { + BlockID toPlace; + int x, y, z; + + toPlace = (BlockID)cuboid_block; + if (cuboid_block == -1) toPlace = Inventory_SelectedBlock; + + for (y = min.y; y <= max.y; y++) { + for (z = min.z; z <= max.z; z++) { + for (x = min.x; x <= max.x; x++) { + Game_ChangeBlock(x, y, z, toPlace); + } + } + } +} + +static void CuboidCommand_Execute(const cc_string* args, int argsCount) { + cc_string value = *args; + + DrawOpCommand_ResetState(); + drawOp_name = "Cuboid"; + drawOp_Func = CuboidCommand_Draw; + + DrawOpCommand_ExtractPersistArg(&value); + cuboid_block = -1; /* Default to cuboiding with currently held block */ + + if (value.length) { + cuboid_block = DrawOpCommand_ParseBlock(&value); + if (cuboid_block == -1) return; + } + + DrawOpCommand_Begin(); +} + +static struct ChatCommand CuboidCommand = { + "Cuboid", CuboidCommand_Execute, + COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client cuboid [block] [persist]", + "&eFills the 3D rectangle between two points with [block].", + "&eIf no block is given, uses your currently held block.", + "&e If persist is given and is \"yes\", then the command", + "&e will repeatedly cuboid, without needing to be typed in again.", + } +}; + + +/*########################################################################################################################* +*-------------------------------------------------------ReplaceCommand-----------------------------------------------------* +*#########################################################################################################################*/ +static int replace_source, replace_target; + +static void ReplaceCommand_Draw(IVec3 min, IVec3 max) { + BlockID cur, source, toPlace; + int x, y, z; + + source = (BlockID)replace_source; + toPlace = (BlockID)replace_target; + if (replace_target == -1) toPlace = Inventory_SelectedBlock; + + for (y = min.y; y <= max.y; y++) { + for (z = min.z; z <= max.z; z++) { + for (x = min.x; x <= max.x; x++) { + cur = World_GetBlock(x, y, z); + if (cur != source) continue; + Game_ChangeBlock(x, y, z, toPlace); + } + } + } +} + +static void ReplaceCommand_Execute(const cc_string* args, int argsCount) { + cc_string value = *args; + cc_string parts[2]; + int count; + + DrawOpCommand_ResetState(); + drawOp_name = "Replace"; + drawOp_Func = ReplaceCommand_Draw; + + DrawOpCommand_ExtractPersistArg(&value); + replace_target = -1; /* Default to replacing with currently held block */ + + if (!value.length) { + Chat_AddRaw("&eReplace: &cAt least one argument is required"); return; + } + count = String_UNSAFE_Split(&value, ' ', parts, 2); + + replace_source = DrawOpCommand_ParseBlock(&parts[0]); + if (replace_source == -1) return; + + if (count > 1) { + replace_target = DrawOpCommand_ParseBlock(&parts[1]); + if (replace_target == -1) return; + } + + DrawOpCommand_Begin(); +} + +static struct ChatCommand ReplaceCommand = { + "Replace", ReplaceCommand_Execute, + COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client replace [source] [replacement] [persist]", + "&eReplaces all [source] blocks between two points with [replacement].", + "&eIf no [replacement] is given, replaces with your currently held block.", + "&e If persist is given and is \"yes\", then the command", + "&e will repeatedly replace, without needing to be typed in again.", + } +}; + + +/*########################################################################################################################* +*------------------------------------------------------TeleportCommand----------------------------------------------------* +*#########################################################################################################################*/ +static void TeleportCommand_Execute(const cc_string* args, int argsCount) { + struct Entity* e = &Entities.CurPlayer->Base; + struct LocationUpdate update; + Vec3 v; + + if (argsCount != 3) { + Chat_AddRaw("&e/client teleport: &cYou didn't specify X, Y and Z coordinates."); + return; + } + if (!Convert_ParseFloat(&args[0], &v.x) || !Convert_ParseFloat(&args[1], &v.y) || !Convert_ParseFloat(&args[2], &v.z)) { + Chat_AddRaw("&e/client teleport: &cCoordinates must be decimals"); + return; + } + + update.flags = LU_HAS_POS; + update.pos = v; + e->VTABLE->SetLocation(e, &update); +} + +static struct ChatCommand TeleportCommand = { + "TP", TeleportCommand_Execute, + COMMAND_FLAG_SINGLEPLAYER_ONLY, + { + "&a/client tp [x y z]", + "&eMoves you to the given coordinates.", + } +}; + + +/*########################################################################################################################* +*------------------------------------------------------BlockEditCommand----------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool BlockEditCommand_GetInt(const cc_string* str, const char* name, int* value, int min, int max) { + if (!Convert_ParseInt(str, value)) { + Chat_Add1("&eBlockEdit: &e%c must be an integer", name); + return false; + } + + if (*value < min || *value > max) { + Chat_Add3("&eBlockEdit: &e%c must be between %i and %i", name, &min, &max); + return false; + } + return true; +} + +static cc_bool BlockEditCommand_GetTexture(const cc_string* str, int* tex) { + return BlockEditCommand_GetInt(str, "Texture", tex, 0, ATLAS1D_MAX_ATLASES - 1); +} + +static cc_bool BlockEditCommand_GetCoords(const cc_string* str, Vec3* coords) { + cc_string parts[3]; + IVec3 xyz; + + int numParts = String_UNSAFE_Split(str, ' ', parts, 3); + if (numParts != 3) { + Chat_AddRaw("&eBlockEdit: &c3 values are required for a coordinate (X Y Z)"); + return false; + } + + if (!BlockEditCommand_GetInt(&parts[0], "X coord", &xyz.x, -127, 127)) return false; + if (!BlockEditCommand_GetInt(&parts[1], "Y coord", &xyz.y, -127, 127)) return false; + if (!BlockEditCommand_GetInt(&parts[2], "Z coord", &xyz.z, -127, 127)) return false; + + coords->x = xyz.x / 16.0f; + coords->y = xyz.y / 16.0f; + coords->z = xyz.z / 16.0f; + return true; +} + +static cc_bool BlockEditCommand_GetBool(const cc_string* str, const char* name, cc_bool* value) { + if (String_CaselessEqualsConst(str, "true") || String_CaselessEqualsConst(str, "yes")) { + *value = true; + return true; + } + + if (String_CaselessEqualsConst(str, "false") || String_CaselessEqualsConst(str, "no")) { + *value = false; + return true; + } + + Chat_Add1("&eBlockEdit: &e%c must be either &ayes &eor &ano", name); + return false; +} + + +static void BlockEditCommand_Execute(const cc_string* args, int argsCount__) { + cc_string parts[3]; + cc_string* prop; + cc_string* value; + int argsCount, block, v; + cc_bool b; + Vec3 coords; + + if (String_CaselessEqualsConst(args, "properties")) { + Chat_AddRaw("&eEditable block properties:"); + Chat_AddRaw("&a name &e- Sets the name of the block"); + Chat_AddRaw("&a all &e- Sets textures on all six sides of the block"); + Chat_AddRaw("&a sides &e- Sets textures on four sides of the block"); + Chat_AddRaw("&a left/right/front/back/top/bottom &e- Sets one texture"); + Chat_AddRaw("&a collide &e- Sets collision mode of the block"); + Chat_AddRaw("&a drawmode &e- Sets draw mode of the block"); + Chat_AddRaw("&a min/max &e- Sets min/max corner coordinates of the block"); + Chat_AddRaw("&eSee &a/client blockedit properties 2 &efor more properties"); + return; + } + if (String_CaselessEqualsConst(args, "properties 2")) { + Chat_AddRaw("&eEditable block properties (page 2):"); + Chat_AddRaw("&a walksound &e- Sets walk/step sound of the block"); + Chat_AddRaw("&a breaksound &e- Sets break sound of the block"); + Chat_AddRaw("&a fullbright &e- Sets whether the block is fully lit"); + Chat_AddRaw("&a blockslight &e- Sets whether the block stops light"); + return; + } + + argsCount = String_UNSAFE_Split(args, ' ', parts, 3); + if (argsCount < 3) { + Chat_AddRaw("&eBlockEdit: &eThree arguments required &e(See &a/client help blockedit&e)"); + return; + } + + block = Block_Parse(&parts[0]); + if (block == -1) { + Chat_Add1("&eBlockEdit: &c\"%s\" is not a valid block name or ID", &parts[0]); + return; + } + + /* TODO: Redo as an array */ + prop = &parts[1]; + value = &parts[2]; + if (String_CaselessEqualsConst(prop, "name")) { + Block_SetName(block, value); + } else if (String_CaselessEqualsConst(prop, "all")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_SetSide(v, block); + Block_Tex(block, FACE_YMAX) = v; + Block_Tex(block, FACE_YMIN) = v; + } else if (String_CaselessEqualsConst(prop, "sides")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_SetSide(v, block); + } else if (String_CaselessEqualsConst(prop, "left")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_Tex(block, FACE_XMIN) = v; + } else if (String_CaselessEqualsConst(prop, "right")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_Tex(block, FACE_XMAX) = v; + } else if (String_CaselessEqualsConst(prop, "bottom")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_Tex(block, FACE_YMIN) = v; + } else if (String_CaselessEqualsConst(prop, "top")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_Tex(block, FACE_YMAX) = v; + } else if (String_CaselessEqualsConst(prop, "front")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_Tex(block, FACE_ZMIN) = v; + } else if (String_CaselessEqualsConst(prop, "back")) { + if (!BlockEditCommand_GetTexture(value, &v)) return; + + Block_Tex(block, FACE_ZMAX) = v; + } else if (String_CaselessEqualsConst(prop, "collide")) { + if (!BlockEditCommand_GetInt(value, "Collide mode", &v, 0, COLLIDE_CLIMB)) return; + + Blocks.Collide[block] = v; + } else if (String_CaselessEqualsConst(prop, "drawmode")) { + if (!BlockEditCommand_GetInt(value, "Draw mode", &v, 0, DRAW_SPRITE)) return; + + Blocks.Draw[block] = v; + } else if (String_CaselessEqualsConst(prop, "min")) { + if (!BlockEditCommand_GetCoords(value, &coords)) return; + + Blocks.MinBB[block] = coords; + } else if (String_CaselessEqualsConst(prop, "max")) { + if (!BlockEditCommand_GetCoords(value, &coords)) return; + + Blocks.MaxBB[block] = coords; + } else if (String_CaselessEqualsConst(prop, "walksound")) { + if (!BlockEditCommand_GetInt(value, "Sound", &v, 0, SOUND_COUNT - 1)) return; + + Blocks.StepSounds[block] = v; + } else if (String_CaselessEqualsConst(prop, "breaksound")) { + if (!BlockEditCommand_GetInt(value, "Sound", &v, 0, SOUND_COUNT - 1)) return; + + Blocks.DigSounds[block] = v; + } else if (String_CaselessEqualsConst(prop, "fullbright")) { + if (!BlockEditCommand_GetBool(value, "Full brightness", &b)) return; + //TODO: Fix this, brightness isn't just a bool anymore + Blocks.Brightness[block] = b; + } else if (String_CaselessEqualsConst(prop, "blockslight")) { + if (!BlockEditCommand_GetBool(value, "Blocks light", &b)) return; + + Blocks.BlocksLight[block] = b; + } else { + Chat_Add1("&eBlockEdit: &eUnknown property %s &e(See &a/client help blockedit&e)", prop); + return; + } + + Block_DefineCustom(block, false); +} + +static struct ChatCommand BlockEditCommand = { + "BlockEdit", BlockEditCommand_Execute, + COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS, + { + "&a/client blockedit [block] [property] [value]", + "&eEdits the given property of the given block", + "&a/client blockedit properties", + "&eLists the editable block properties", + } +}; + + +/*########################################################################################################################* +*------------------------------------------------------Commands component-------------------------------------------------* +*#########################################################################################################################*/ +static void OnInit(void) { + Commands_Register(&GpuInfoCommand); + Commands_Register(&HelpCommand); + Commands_Register(&RenderTypeCommand); + Commands_Register(&ResolutionCommand); + Commands_Register(&ModelCommand); + Commands_Register(&TeleportCommand); + Commands_Register(&ClearDeniedCommand); + Commands_Register(&MotdCommand); + Commands_Register(&BlockEditCommand); + Commands_Register(&CuboidCommand); + Commands_Register(&ReplaceCommand); +} + +static void OnFree(void) { + cmds_head = NULL; +} + +struct IGameComponent Commands_Component = { + OnInit, /* Init */ + OnFree /* Free */ +}; diff --git a/src/Commands.h b/src/Commands.h new file mode 100644 index 0000000..e6cbfc7 --- /dev/null +++ b/src/Commands.h @@ -0,0 +1,31 @@ +#ifndef CC_COMMANDS_H +#define CC_COMMANDS_H +#include "Core.h" +/* Executes actions in response to certain chat input + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent Commands_Component; + +cc_bool Commands_Execute(const cc_string* input); + +/* This command is only available in singleplayer */ +#define COMMAND_FLAG_SINGLEPLAYER_ONLY 0x01 +/* args is passed as a single string instead of being split by spaces */ +#define COMMAND_FLAG_UNSPLIT_ARGS 0x02 + +struct ChatCommand; +/* Represents a client-side command/action */ +struct ChatCommand { + const char* name; /* Full name of this command */ + /* Function pointer for the actual action the command performs */ + void (*Execute)(const cc_string* args, int argsCount); + cc_uint8 flags; /* Flags for handling this command (see COMMAND_FLAG defines) */ + const char* help[5]; /* Messages to show when a player uses /help on this command */ + struct ChatCommand* next; /* Next command in linked-list of client commands */ +}; + +/* Registers a client-side command, allowing it to be used with /client [cmd name] */ +CC_API void Commands_Register( struct ChatCommand* cmd); +typedef void (*FP_Commands_Register)(struct ChatCommand* cmd); +#endif diff --git a/src/Constants.h b/src/Constants.h new file mode 100644 index 0000000..46677ac --- /dev/null +++ b/src/Constants.h @@ -0,0 +1,78 @@ +#ifndef CC_CONSTANTS_H +#define CC_CONSTANTS_H +/* +Defines useful constants +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +#define GAME_MAX_CMDARGS 5 +#define GAME_APP_VER "0.1.69b" +#define GAME_API_VER 1 + +#if defined CC_BUILD_WEB +#define GAME_APP_ALT "ClassiHax 0.1.69b web mobile" +#define GAME_APP_NAME "ClassiHax 0.1.69b web" +#define GAME_APP_TITLE "ClassiHax" +#else +#define GAME_APP_NAME "ClassiHax 0.1.69b" +#define GAME_APP_TITLE "ClassiHax 0.1.69b" +#endif + +/* Max number of characters strings can have. */ +#define STRING_SIZE 64 +/* Max number of characters filenames can have. */ +#define FILENAME_SIZE 260 + +/* Chunk axis length in blocks. */ +#define CHUNK_SIZE 16 +#define HALF_CHUNK_SIZE 8 +#define CHUNK_SIZE_2 (CHUNK_SIZE * CHUNK_SIZE) +#define CHUNK_SIZE_3 (CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE) + +#define CHUNK_MAX 15 +/* Local index in a chunk for a coordinate. */ +#define CHUNK_MASK 15 +/* Chunk index for a coordinate. */ +#define CHUNK_SHIFT 4 + +/* Chunk axis length (plus neighbours) in blocks. */ +#define EXTCHUNK_SIZE 18 +#define EXTCHUNK_SIZE_2 (EXTCHUNK_SIZE * EXTCHUNK_SIZE) +#define EXTCHUNK_SIZE_3 (EXTCHUNK_SIZE * EXTCHUNK_SIZE * EXTCHUNK_SIZE) + +/* Minor adjustment to max UV coords, to avoid pixel bleeding errors due to rounding. */ +#define UV2_Scale (15.99f / 16.0f) + +#define GAME_DEF_TICKS (1.0 / 20) +#define GAME_NET_TICKS (1.0 / 60) + +#define GUI_MAX_CHATLINES 30 + +enum FACE_CONSTS { + FACE_XMIN = 0, /* Face X = 0 */ + FACE_XMAX = 1, /* Face X = 1 */ + FACE_ZMIN = 2, /* Face Z = 0 */ + FACE_ZMAX = 3, /* Face Z = 1 */ + FACE_YMIN = 4, /* Face Y = 0 */ + FACE_YMAX = 5, /* Face Y = 1 */ + FACE_COUNT= 6 /* Number of faces on a cube */ +}; + +enum SKIN_TYPE { SKIN_64x32, SKIN_64x64, SKIN_64x64_SLIM, SKIN_INVALID = 0xF0 }; +#define DRAWER2D_MAX_COLORS 256 + +#define UInt8_MaxValue ((cc_uint8)255) +#define Int16_MaxValue ((cc_int16)32767) +#define UInt16_MaxValue ((cc_uint16)65535) +#define Int32_MinValue ((cc_int32)-2147483647L - (cc_int32)1L) +#define Int32_MaxValue ((cc_int32)2147483647L) + +#define SKINS_SERVER "http://cdn.classicube.net/skin" +#define UPDATES_SERVER "http://cdn.classicube.net/client" +#define SERVICES_SERVER "https://www.classicube.net/api" +#define RESOURCE_SERVER "http://static.classicube.net" +/* Webpage where users can register for a new account */ +#define REGISTERNEW_URL "https://www.classicube.net/acc/register/" + +#define DEFAULT_USERNAME "Singleplayer" +#endif diff --git a/src/Core.h b/src/Core.h new file mode 100644 index 0000000..3b2c4d3 --- /dev/null +++ b/src/Core.h @@ -0,0 +1,494 @@ +#ifndef CC_CORE_H +#define CC_CORE_H +/* +Core fixed-size integer types, automatic platform detection, and common small structs +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +#if _MSC_VER + typedef signed __int8 cc_int8; + typedef signed __int16 cc_int16; + typedef signed __int32 cc_int32; + typedef signed __int64 cc_int64; + + typedef unsigned __int8 cc_uint8; + typedef unsigned __int16 cc_uint16; + typedef unsigned __int32 cc_uint32; + typedef unsigned __int64 cc_uint64; + #ifdef _WIN64 + typedef unsigned __int64 cc_uintptr; + #else + typedef unsigned __int32 cc_uintptr; + #endif + + #define CC_INLINE inline + #define CC_NOINLINE __declspec(noinline) + #ifndef CC_API + #define CC_API __declspec(dllexport, noinline) + #define CC_VAR __declspec(dllexport) + #endif + + #define CC_HAS_TYPES + #define CC_HAS_MISC +#elif __GNUC__ + /* really old GCC/clang might not have these defined */ + #ifdef __INT8_TYPE__ + /* avoid including because it breaks defining UNICODE in Platform.c with MinGW */ + typedef __INT8_TYPE__ cc_int8; + typedef __INT16_TYPE__ cc_int16; + typedef __INT32_TYPE__ cc_int32; + typedef __INT64_TYPE__ cc_int64; + + #ifdef __UINT8_TYPE__ + typedef __UINT8_TYPE__ cc_uint8; + typedef __UINT16_TYPE__ cc_uint16; + typedef __UINT32_TYPE__ cc_uint32; + typedef __UINT64_TYPE__ cc_uint64; + typedef __UINTPTR_TYPE__ cc_uintptr; + #else + /* clang doesn't define the __UINT8_TYPE__ */ + typedef unsigned __INT8_TYPE__ cc_uint8; + typedef unsigned __INT16_TYPE__ cc_uint16; + typedef unsigned __INT32_TYPE__ cc_uint32; + typedef unsigned __INT64_TYPE__ cc_uint64; + typedef unsigned __INTPTR_TYPE__ cc_uintptr; + #endif + #define CC_HAS_TYPES + #endif + + #define CC_INLINE inline + #define CC_NOINLINE __attribute__((noinline)) + #ifndef CC_API + #ifdef _WIN32 + #define CC_API __attribute__((dllexport, noinline)) + #define CC_VAR __attribute__((dllexport)) + #else + #define CC_API __attribute__((visibility("default"), noinline)) + #define CC_VAR __attribute__((visibility("default"))) + #endif + #endif + #define CC_HAS_MISC + #ifdef __BIG_ENDIAN__ + #define CC_BIG_ENDIAN + #endif +#elif __MWERKS__ + /* TODO: Is there actual attribute support for CC_API etc somewhere? */ + #define CC_BIG_ENDIAN +#endif + +/* Unrecognised compiler, so just go with some sensible default typdefs */ +/* Don't use , as good chance such a compiler doesn't support it */ +#ifndef CC_HAS_TYPES +typedef signed char cc_int8; +typedef signed short cc_int16; +typedef signed int cc_int32; +typedef signed long long cc_int64; + +typedef unsigned char cc_uint8; +typedef unsigned short cc_uint16; +typedef unsigned int cc_uint32; +typedef unsigned long long cc_uint64; +typedef unsigned long cc_uintptr; +#endif +#ifndef CC_HAS_MISC +#define CC_INLINE +#define CC_NOINLINE +#define CC_API +#define CC_VAR +#endif + +typedef cc_uint32 cc_codepoint; +typedef cc_uint16 cc_unichar; +typedef cc_uint8 cc_bool; +#ifdef __APPLE__ +/* TODO: REMOVE THIS AWFUL AWFUL HACK */ +#include +#elif __cplusplus +#else +#define true 1 +#define false 0 +#endif + +#ifndef NULL +#if __cplusplus +#define NULL 0 +#else +#define NULL ((void*)0) +#endif +#endif + +/* Lowest 4 bits are for backends in same group */ +/* Rest of the bits indicate the unique group type */ + +#define CC_WIN_BACKEND_TERMINAL 0x0001 +#define CC_WIN_BACKEND_SDL_MASK 0x0010 +#define CC_WIN_BACKEND_SDL2 0x0011 +#define CC_WIN_BACKEND_SDL3 0x0012 +#define CC_WIN_BACKEND_X11 0x0021 +#define CC_WIN_BACKEND_WIN32 0x0041 +#define CC_WIN_BACKEND_COCOA 0x0081 +#define CC_WIN_BACKEND_BEOS 0x0101 +#define CC_WIN_BACKEND_ANDROID 0x0201 + +#define CC_GFX_BACKEND_SOFTGPU 0x0001 +#define CC_GFX_BACKEND_GL_MASK 0x0010 +#define CC_GFX_BACKEND_GL1 0x0011 +#define CC_GFX_BACKEND_GL2 0x0012 +#define CC_GFX_BACKEND_D3D9 0x0021 +#define CC_GFX_BACKEND_D3D11 0x0041 +#define CC_GFX_BACKEND_VULKAN 0x0081 + +#define CC_BUILD_NETWORKING +#define CC_BUILD_FREETYPE +#define CC_BUILD_RESOURCES +#define CC_BUILD_PLUGINS +#define CC_BUILD_ANIMATIONS +#define CC_BUILD_FILESYSTEM +#define CC_BUILD_ADVLIGHTING +/*#define CC_BUILD_GL11*/ + +#ifndef CC_BUILD_MANUAL +#if defined NXDK + /* XBox also defines _WIN32 */ + #define CC_BUILD_XBOX + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_SPLITSCREEN +#elif defined XENON + /* libxenon also defines __linux__ (yes, really) */ + #define CC_BUILD_XBOX360 + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #define CC_BUILD_HTTPCLIENT +#elif defined _WIN32 + #define CC_BUILD_WIN + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_SCHANNEL + #define CC_BUILD_WINMM + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_D3D9 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_WIN32 +#elif defined __ANDROID__ + #define CC_BUILD_ANDROID + #define CC_BUILD_MOBILE + #define CC_BUILD_POSIX + #define CC_BUILD_GLES + #define CC_BUILD_EGL + #define CC_BUILD_TOUCH + #define CC_BUILD_OPENSLES + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL2 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_ANDROID +#elif defined __serenity__ + #define CC_BUILD_SERENITY + #define CC_BUILD_POSIX + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_SDL2 +#elif defined __linux__ + #define CC_BUILD_LINUX + #define CC_BUILD_POSIX + #define CC_BUILD_XINPUT2 + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_X11 + #if defined CC_BUILD_RPI + #define CC_BUILD_GLES + #define CC_BUILD_EGL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL2 + #else + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #endif +#elif defined __APPLE__ + #define CC_BUILD_DARWIN + #define CC_BUILD_POSIX + #if defined __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ + #define CC_BUILD_MOBILE + #define CC_BUILD_GLES + #define CC_BUILD_IOS + #define CC_BUILD_TOUCH + #define CC_BUILD_CFNETWORK + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL2 + #else + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_COCOA + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define CC_BUILD_MACOS + #define CC_BUILD_CURL + #endif + #define CC_BUILD_OPENAL +#elif defined Macintosh + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_SOFTGPU + #define CC_BUILD_MACCLASSIC + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #undef CC_BUILD_RESOURCES + #undef CC_BUILD_FREETYPE + #undef CC_BUILD_NETWORKING +#elif defined __sun__ + #define CC_BUILD_SOLARIS + #define CC_BUILD_POSIX + #define CC_BUILD_XINPUT2 + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_X11 +#elif defined __FreeBSD__ || defined __DragonFly__ + #define CC_BUILD_FREEBSD + #define CC_BUILD_POSIX + #define CC_BUILD_BSD + #define CC_BUILD_XINPUT2 + #define CC_BUILD_CURL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_X11 +#elif defined __OpenBSD__ + #define CC_BUILD_OPENBSD + #define CC_BUILD_POSIX + #define CC_BUILD_BSD + #define CC_BUILD_XINPUT2 + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_X11 +#elif defined __NetBSD__ + #define CC_BUILD_NETBSD + #define CC_BUILD_POSIX + #define CC_BUILD_BSD + #define CC_BUILD_XINPUT2 + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_X11 +#elif defined __HAIKU__ + #define CC_BUILD_HAIKU + #define CC_BUILD_POSIX + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define CC_BACKTRACE_BUILTIN + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_BEOS +#elif defined __BEOS__ + #define CC_BUILD_BEOS + #define CC_BUILD_POSIX + #define CC_BUILD_GL11 + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_OPENAL + #define CC_BACKTRACE_BUILTIN + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_BEOS +#elif defined __sgi + #define CC_BUILD_IRIX + #define CC_BUILD_POSIX + #define CC_BUILD_CURL + #define CC_BUILD_OPENAL + #define CC_BIG_ENDIAN + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL1 + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_X11 +#elif defined __EMSCRIPTEN__ + #define CC_BUILD_WEB + #define CC_BUILD_GLES + #define CC_BUILD_TOUCH + #define CC_BUILD_WEBAUDIO + #define CC_BUILD_NOMUSIC + #define CC_BUILD_MINFILES + #define CC_BUILD_COOPTHREADED + #undef CC_BUILD_FREETYPE + #undef CC_BUILD_RESOURCES + #undef CC_BUILD_PLUGINS + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL2 +#elif defined __psp__ + #define CC_BUILD_PSP + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_OPENAL + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL +#elif defined __3DS__ + #define CC_BUILD_3DS + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_TOUCH + #define CC_BUILD_DUALSCREEN +#elif defined GEKKO + #define CC_BUILD_GCWII + #define CC_BUILD_CONSOLE + #ifndef HW_RVL + #define CC_BUILD_LOWMEM + #endif + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_SPLITSCREEN +#elif defined __vita__ + #define CC_BUILD_PSVITA + #define CC_BUILD_CONSOLE + #define CC_BUILD_OPENAL + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_TOUCH +#elif defined _arch_dreamcast + #define CC_BUILD_DREAMCAST + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_SPLITSCREEN + #undef CC_BUILD_RESOURCES +#elif defined PLAT_PS3 + #define CC_BUILD_PS3 + #define CC_BUILD_CONSOLE + #define CC_BUILD_OPENAL + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_SPLITSCREEN +#elif defined N64 + #define CC_BIG_ENDIAN + #define CC_BUILD_N64 + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #undef CC_BUILD_RESOURCES + #undef CC_BUILD_NETWORKING + #undef CC_BUILD_FILESYSTEM +#elif defined PLAT_PS2 + #define CC_BUILD_PS2 + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_OPENAL + #define CC_BUILD_HTTPCLIENT +#elif defined PLAT_NDS + #define CC_BUILD_NDS + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_TOUCH + #undef CC_BUILD_RESOURCES + #undef CC_BUILD_ANIMATIONS /* Very costly in FPU less system */ + #undef CC_BUILD_ADVLIGHTING +#elif defined __WIIU__ + #define CC_BUILD_WIIU + #define CC_BUILD_CONSOLE + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_OPENAL + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_SPLITSCREEN + #define CC_BUILD_TOUCH +#elif defined __SWITCH__ + #define CC_BUILD_SWITCH + #define CC_BUILD_CONSOLE + #define CC_BUILD_HTTPCLIENT + #define CC_BUILD_BEARSSL + #define CC_BUILD_TOUCH + #define CC_BUILD_GLES + #define CC_BUILD_EGL + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_GL2 +#elif defined PLAT_PS1 + #define CC_BUILD_PS1 + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #undef CC_BUILD_RESOURCES + #undef CC_BUILD_NETWORKING + #undef CC_BUILD_ANIMATIONS /* Very costly in FPU less system */ + #undef CC_BUILD_ADVLIGHTING + #undef CC_BUILD_FILESYSTEM +#elif defined OS2 + #define CC_BUILD_OS2 + #define CC_BUILD_POSIX + #define CC_BUILD_CURL + #define CC_BUILD_FREETYPE + #define DEFAULT_GFX_BACKEND CC_GFX_BACKEND_SOFTGPU + #define DEFAULT_WIN_BACKEND CC_WIN_BACKEND_SDL2 +#elif defined PLAT_SATURN + #define CC_BUILD_SATURN + #define CC_BUILD_CONSOLE + #define CC_BUILD_LOWMEM + #define CC_BUILD_COOPTHREADED + #define CC_BUILD_NOMUSIC + #define CC_BUILD_NOSOUNDS + #undef CC_BUILD_RESOURCES + #undef CC_BUILD_NETWORKING + #undef CC_BUILD_ANIMATIONS /* Very costly in FPU less system */ + #undef CC_BUILD_ADVLIGHTING + #undef CC_BUILD_FILESYSTEM +#endif +#endif + +/* Use platform default unless override is provided via command line/makefile/etc */ +#if defined DEFAULT_WIN_BACKEND && !defined CC_WIN_BACKEND + #define CC_WIN_BACKEND DEFAULT_WIN_BACKEND +#endif +#if defined DEFAULT_GFX_BACKEND && !defined CC_GFX_BACKEND + #define CC_GFX_BACKEND DEFAULT_GFX_BACKEND +#endif + +#ifdef CC_BUILD_CONSOLE +#undef CC_BUILD_FREETYPE +#undef CC_BUILD_PLUGINS +#endif + +#ifdef CC_BUILD_NETWORKING +#define CUSTOM_MODELS +#endif +#ifndef CC_BUILD_LOWMEM +#define EXTENDED_BLOCKS +#endif +#define EXTENDED_TEXTURES + +#ifdef EXTENDED_BLOCKS +typedef cc_uint16 BlockID; +#else +typedef cc_uint8 BlockID; +#endif + +#ifdef EXTENDED_TEXTURES +typedef cc_uint16 TextureLoc; +#else +typedef cc_uint8 TextureLoc; +#endif + +typedef cc_uint8 BlockRaw; +typedef cc_uint8 EntityID; +typedef cc_uint8 Face; +typedef cc_uint32 cc_result; +typedef cc_uint64 TimeMS; + +typedef struct Rect2D_ { int x, y, width, height; } Rect2D; +typedef struct TextureRec_ { float u1, v1, u2, v2; } TextureRec; + +typedef struct cc_string_ { + char* buffer; /* Pointer to characters, NOT NULL TERMINATED */ + cc_uint16 length; /* Number of characters used */ + cc_uint16 capacity; /* Max number of characters */ +} cc_string; +/* Indicates that a reference to the buffer in a string argument is persisted after the function has completed. +Thus it is **NOT SAFE** to allocate a string on the stack. */ +#define STRING_REF + +typedef void* GfxResourceID; + +/* Contains the information to describe a 2D textured quad. */ +struct Texture { + GfxResourceID ID; + short x, y; cc_uint16 width, height; + TextureRec uv; +}; +#endif diff --git a/src/Deflate.c b/src/Deflate.c new file mode 100644 index 0000000..247b445 --- /dev/null +++ b/src/Deflate.c @@ -0,0 +1,1317 @@ +#include "Deflate.h" +#include "String.h" +#include "Logger.h" +#include "Funcs.h" +#include "Platform.h" +#include "Stream.h" +#include "Errors.h" +#include "Utils.h" + +#define Header_ReadU8(value) if ((res = s->ReadU8(s, &value))) return res; +/*########################################################################################################################* +*-------------------------------------------------------GZip header-------------------------------------------------------* +*#########################################################################################################################*/ +enum GzipState { + GZIP_STATE_HEADER1, GZIP_STATE_HEADER2, GZIP_STATE_COMPRESSIONMETHOD, GZIP_STATE_FLAGS, + GZIP_STATE_LASTMODIFIED, GZIP_STATE_COMPRESSIONFLAGS, GZIP_STATE_OPERATINGSYSTEM, + GZIP_STATE_HEADERCHECKSUM, GZIP_STATE_FILENAME, GZIP_STATE_COMMENT, GZIP_STATE_DONE +}; + +void GZipHeader_Init(struct GZipHeader* header) { + header->state = GZIP_STATE_HEADER1; + header->done = false; + header->flags = 0; + header->partsRead = 0; +} + +cc_result GZipHeader_Read(struct Stream* s, struct GZipHeader* header) { + cc_uint8 tmp; + cc_result res; + switch (header->state) { + + case GZIP_STATE_HEADER1: + Header_ReadU8(tmp); + if (tmp != 0x1F) return GZIP_ERR_HEADER1; + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_HEADER2: + Header_ReadU8(tmp); + if (tmp != 0x8B) return GZIP_ERR_HEADER2; + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_COMPRESSIONMETHOD: + Header_ReadU8(tmp); + if (tmp != 0x08) return GZIP_ERR_METHOD; + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_FLAGS: + Header_ReadU8(tmp); + header->flags = tmp; + if (header->flags & 0x04) return GZIP_ERR_FLAGS; + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_LASTMODIFIED: + for (; header->partsRead < 4; header->partsRead++) { + Header_ReadU8(tmp); + } + header->state++; + header->partsRead = 0; + + /* FALLTHRU */ + case GZIP_STATE_COMPRESSIONFLAGS: + Header_ReadU8(tmp); + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_OPERATINGSYSTEM: + Header_ReadU8(tmp); + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_FILENAME: + if (header->flags & 0x08) { + for (; ;) { + Header_ReadU8(tmp); + if (tmp == '\0') break; + } + } + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_COMMENT: + if (header->flags & 0x10) { + for (; ;) { + Header_ReadU8(tmp); + if (tmp == '\0') break; + } + } + header->state++; + + /* FALLTHRU */ + case GZIP_STATE_HEADERCHECKSUM: + if (header->flags & 0x02) { + for (; header->partsRead < 2; header->partsRead++) { + Header_ReadU8(tmp); + } + } + header->state++; + header->partsRead = 0; + header->done = true; + } + return 0; +} + + +/*########################################################################################################################* +*-------------------------------------------------------ZLib header-------------------------------------------------------* +*#########################################################################################################################*/ +enum ZlibState { ZLIB_STATE_COMPRESSIONMETHOD, ZLIB_STATE_FLAGS, ZLIB_STATE_DONE }; + +void ZLibHeader_Init(struct ZLibHeader* header) { + header->state = ZLIB_STATE_COMPRESSIONMETHOD; + header->done = false; +} + +cc_result ZLibHeader_Read(struct Stream* s, struct ZLibHeader* header) { + cc_uint8 tmp; + cc_result res; + switch (header->state) { + + case ZLIB_STATE_COMPRESSIONMETHOD: + Header_ReadU8(tmp); + if ((tmp & 0x0F) != 0x08) return ZLIB_ERR_METHOD; + /* Upper 4 bits are window size (ignored) */ + header->state++; + + /* FALLTHRU */ + case ZLIB_STATE_FLAGS: + Header_ReadU8(tmp); + if (tmp & 0x20) return ZLIB_ERR_FLAGS; + header->state++; + header->done = true; + } + return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------Inflate (decompress)---------------------------------------------------* +*#########################################################################################################################*/ +enum INFLATE_STATE_ { + INFLATE_STATE_HEADER, INFLATE_STATE_UNCOMPRESSED_HEADER, + INFLATE_STATE_UNCOMPRESSED_DATA, INFLATE_STATE_DYNAMIC_HEADER, + INFLATE_STATE_DYNAMIC_CODELENS, INFLATE_STATE_DYNAMIC_LITSDISTS, + INFLATE_STATE_DYNAMIC_LITSDISTSREPEAT, INFLATE_STATE_COMPRESSED_LIT, + INFLATE_STATE_COMPRESSED_LITEXTRA, INFLATE_STATE_COMPRESSED_DIST, + INFLATE_STATE_COMPRESSED_DISTEXTRA, INFLATE_STATE_COMPRESSED_DATA, + INFLATE_STATE_FASTCOMPRESSED, INFLATE_STATE_DONE +}; + +/* Insert next byte into the bit buffer */ +#define Inflate_GetByte(state) state->AvailIn--; state->Bits |= (cc_uint32)(*state->NextIn++) << state->NumBits; state->NumBits += 8; +/* Retrieves bits from the bit buffer */ +#define Inflate_PeekBits(state, bits) (state->Bits & ((1UL << (bits)) - 1UL)) +/* Consumes/eats up bits from the bit buffer */ +#define Inflate_ConsumeBits(state, bits) state->Bits >>= (bits); state->NumBits -= (bits); +/* Aligns bit buffer to be on a byte boundary */ +#define Inflate_AlignBits(state) cc_uint32 alignSkip = state->NumBits & 7; Inflate_ConsumeBits(state, alignSkip); +/* Ensures there are 'bitsCount' bits, or returns if not */ +#define Inflate_EnsureBits(state, bitsCount) while (state->NumBits < bitsCount) { if (!state->AvailIn) return; Inflate_GetByte(state); } +/* Ensures there are 'bitsCount' bits */ +#define Inflate_UNSAFE_EnsureBits(state, bitsCount) while (state->NumBits < bitsCount) { Inflate_GetByte(state); } +/* Peeks then consumes given bits */ +#define Inflate_ReadBits(state, bitsCount) Inflate_PeekBits(state, bitsCount); Inflate_ConsumeBits(state, bitsCount); +/* Sets to given result and sets state to DONE */ +#define Inflate_Fail(state, res) state->result = res; state->State = INFLATE_STATE_DONE; + +/* Goes to the next state, after having read data of a block */ +#define Inflate_NextBlockState(state) (state->LastBlock ? INFLATE_STATE_DONE : INFLATE_STATE_HEADER) +/* Goes to the next state, after having finished reading a compressed entry */ +#define Inflate_NextCompressState(state) ((state->AvailIn >= INFLATE_FASTINF_IN && state->AvailOut >= INFLATE_FASTINF_OUT) ? INFLATE_STATE_FASTCOMPRESSED : INFLATE_STATE_COMPRESSED_LIT) +/* The maximum amount of bytes that can be output is 258 */ +#define INFLATE_FASTINF_OUT 258 +/* The most input bytes required for huffman codes and extra data is 16 + 5 + 16 + 13 bits. Add 3 extra bytes to account for putting data into the bit buffer. */ +#define INFLATE_FASTINF_IN 10 + +static cc_uint32 Huffman_ReverseBits(cc_uint32 n, cc_uint8 bits) { + n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); + n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); + n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); + n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); + return n >> (16 - bits); +} + +/* Builds a huffman tree, based on input lengths of each codeword */ +static cc_result Huffman_Build(struct HuffmanTable* table, const cc_uint8* bitLens, int count) { + int bl_count[INFLATE_MAX_BITS], bl_offsets[INFLATE_MAX_BITS]; + int code, offset, value; + int i, j; + + /* Initialise 'zero bit length' codewords */ + table->firstCodewords[0] = 0; + table->firstOffsets[0] = 0; + table->endCodewords[0] = 0; + + /* Count number of codewords assigned to each bit length */ + for (i = 0; i < INFLATE_MAX_BITS; i++) bl_count[i] = 0; + for (i = 0; i < count; i++) { + bl_count[bitLens[i]]++; + } + + /* Ensure huffman tree actually makes sense */ + bl_count[0] = 0; + for (i = 1; i < INFLATE_MAX_BITS; i++) { + /* Check if too many huffman codes for bit length */ + if (bl_count[i] > (1 << i)) return INF_ERR_NUM_CODES; + } + + /* Compute the codewords for the huffman tree. + * Codewords are ordered, so consider this example tree: + * 2 of length 2, 3 of length 3, 1 of length 4 + * Codewords produced would be: 00,01 100,101,110, 1110 + */ + code = 0; offset = 0; + for (i = 1; i < INFLATE_MAX_BITS; i++) { + code = (code + bl_count[i - 1]) << 1; + bl_offsets[i] = offset; + + table->firstCodewords[i] = code; + table->firstOffsets[i] = offset; + offset += bl_count[i]; + + /* Last codeword is actually: code + (bl_count[i] - 1) + * However, when decoding we peform < against this value though, so need to add 1 here. + * This way, don't need to special case bit lengths with 0 codewords when decoding. + */ + if (bl_count[i]) { + table->endCodewords[i] = code + bl_count[i]; + } else { + table->endCodewords[i] = 0; + } + } + + /* Assigns values to each codeword. + * Note that although codewords are ordered, values may not be. + * Some values may also not be assigned to any codeword. + */ + value = 0; + Mem_Set(table->fast, UInt8_MaxValue, sizeof(table->fast)); + for (i = 0; i < count; i++, value++) { + int len = bitLens[i]; + if (!len) continue; + table->values[bl_offsets[len]] = value; + + /* Compute the accelerated lookup table values for this codeword. + * For example, assume len = 4 and codeword = 0100 + * - Shift it left to be 0100_00000 + * - Then, for all the indices from 0100_00000 to 0100_11111, + * - bit reverse index, as huffman codes are read backwards + * - set fast value to specify a 'value' value, and to skip 'len' bits + */ + if (len <= INFLATE_FAST_BITS) { + cc_int16 packed = (cc_int16)((len << INFLATE_FAST_LEN_SHIFT) | value); + int codeword = table->firstCodewords[len] + (bl_offsets[len] - table->firstOffsets[len]); + codeword <<= (INFLATE_FAST_BITS - len); + + for (j = 0; j < 1 << (INFLATE_FAST_BITS - len); j++, codeword++) { + int index = Huffman_ReverseBits(codeword, INFLATE_FAST_BITS); + table->fast[index] = packed; + } + } + bl_offsets[len]++; + } + return 0; +} + +/* Attempts to read the next huffman encoded value from the bitstream, using given table */ +/* Returns -1 if there are insufficient bits to read the value */ +static int Huffman_Decode(struct InflateState* state, struct HuffmanTable* table) { + cc_uint32 i, j, codeword; + int packed, bits, offset; + + /* Buffer as many bits as possible */ + while (state->NumBits <= INFLATE_MAX_BITS) { + if (!state->AvailIn) break; + Inflate_GetByte(state); + } + + /* Try fast accelerated table lookup */ + if (state->NumBits >= INFLATE_FAST_BITS) { + packed = table->fast[Inflate_PeekBits(state, INFLATE_FAST_BITS)]; + if (packed >= 0) { + bits = packed >> INFLATE_FAST_LEN_SHIFT; + Inflate_ConsumeBits(state, bits); + return packed & INFLATE_FAST_VAL_MASK; + } + } + + /* Slow, bit by bit lookup */ + codeword = 0; + for (i = 1, j = 0; i < INFLATE_MAX_BITS; i++, j++) { + if (state->NumBits < i) return -1; + codeword = (codeword << 1) | ((state->Bits >> j) & 1); + + if (codeword < table->endCodewords[i]) { + offset = table->firstOffsets[i] + (codeword - table->firstCodewords[i]); + Inflate_ConsumeBits(state, i); + return table->values[offset]; + } + } + + Inflate_Fail(state, INF_ERR_INVALID_CODE); + return -1; +} + +/* Inline the common <= 9 bits case */ +#define Huffman_UNSAFE_Decode(state, table, result) \ +{\ + Inflate_UNSAFE_EnsureBits(state, INFLATE_MAX_BITS);\ + packed = table.fast[Inflate_PeekBits(state, INFLATE_FAST_BITS)];\ + if (packed >= 0) {\ + consumedBits = packed >> INFLATE_FAST_BITS;\ + Inflate_ConsumeBits(state, consumedBits);\ + result = packed & 0x1FF;\ + } else {\ + result = Huffman_UNSAFE_Decode_Slow(state, &table);\ + }\ +} + +static int Huffman_UNSAFE_Decode_Slow(struct InflateState* state, struct HuffmanTable* table) { + cc_uint32 i, j, codeword; + int offset; + + /* Slow, bit by bit lookup. Need to reverse order for huffman. */ + codeword = Inflate_PeekBits(state, INFLATE_FAST_BITS); + codeword = Huffman_ReverseBits(codeword, INFLATE_FAST_BITS); + + for (i = INFLATE_FAST_BITS + 1, j = INFLATE_FAST_BITS; i < INFLATE_MAX_BITS; i++, j++) { + codeword = (codeword << 1) | ((state->Bits >> j) & 1); + + if (codeword < table->endCodewords[i]) { + offset = table->firstOffsets[i] + (codeword - table->firstCodewords[i]); + Inflate_ConsumeBits(state, i); + return table->values[offset]; + } + } + + Inflate_Fail(state, INF_ERR_INVALID_CODE); + /* Need to exit the fast decode loop */ + /* TODO: This means a few garbage bytes can get written */ + /* to the output, but it probably doesn't matter */ + state->AvailIn = 0; + return 0; +} + +void Inflate_Init2(struct InflateState* state, struct Stream* source) { + state->State = INFLATE_STATE_HEADER; + state->LastBlock = false; + state->Bits = 0; + state->NumBits = 0; + state->NextIn = state->Input; + state->AvailIn = 0; + state->Output = NULL; + state->AvailOut = 0; + state->Source = source; + state->WindowIndex = 0; + state->result = 0; +} + +static const cc_uint8 fixed_lits[INFLATE_MAX_LITS] = { + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 +}; +static const cc_uint8 fixed_dists[INFLATE_MAX_DISTS] = { + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 +}; + +static const cc_uint16 len_base[31] = { + 3,4,5,6,7,8,9,10,11,13, + 15,17,19,23,27,31,35,43,51,59, + 67,83,99,115,131,163,195,227,258,0,0 +}; +static const cc_uint8 len_bits[31] = { + 0,0,0,0,0,0,0,0,1,1, + 1,1,2,2,2,2,3,3,3,3, + 4,4,4,4,5,5,5,5,0,0,0 +}; +static const cc_uint16 dist_base[32] = { + 1,2,3,4,5,7,9,13,17,25, + 33,49,65,97,129,193,257,385,513,769, + 1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0 +}; +static const cc_uint8 dist_bits[32] = { + 0,0,0,0,1,1,2,2,3,3, + 4,4,5,5,6,6,7,7,8,8, + 9,9,10,10,11,11,12,12,13,13,0,0 +}; +static const cc_uint8 codelens_order[INFLATE_MAX_CODELENS] = { + 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 +}; + +static void Inflate_InflateFast(struct InflateState* s) { + /* huffman variables */ + cc_uint32 lit, len, dist; + cc_uint32 bits, lenIdx, distIdx; + int packed, consumedBits; + + /* window variables */ + cc_uint8* window; + cc_uint32 i, curIdx, startIdx; + cc_uint32 copyStart, copyLen, partLen; + + window = s->Window; + curIdx = s->WindowIndex; + copyStart = s->WindowIndex; + copyLen = 0; + +#define INFLATE_FAST_COPY_MAX (INFLATE_WINDOW_SIZE - INFLATE_FASTINF_OUT) + while (s->AvailOut >= INFLATE_FASTINF_OUT && s->AvailIn >= INFLATE_FASTINF_IN && copyLen < INFLATE_FAST_COPY_MAX) { + Huffman_UNSAFE_Decode(s, s->Table.Lits, lit); + + if (lit <= 256) { + if (lit < 256) { + window[curIdx] = (cc_uint8)lit; + s->AvailOut--; copyLen++; + curIdx = (curIdx + 1) & INFLATE_WINDOW_MASK; + } else { + s->State = Inflate_NextBlockState(s); + break; + } + } else { + lenIdx = lit - 257; + bits = len_bits[lenIdx]; + Inflate_UNSAFE_EnsureBits(s, bits); + len = len_base[lenIdx] + Inflate_ReadBits(s, bits); + + Huffman_UNSAFE_Decode(s, s->TableDists, distIdx); + bits = dist_bits[distIdx]; + Inflate_UNSAFE_EnsureBits(s, bits); + dist = dist_base[distIdx] + Inflate_ReadBits(s, bits); + + /* Window infinitely repeats like ...xyz|uvwxyz|uvwxyz|uvw... */ + /* If start and end don't cross a boundary, can avoid masking index */ + startIdx = (curIdx - dist) & INFLATE_WINDOW_MASK; + if (curIdx >= startIdx && (curIdx + len) < INFLATE_WINDOW_SIZE) { + cc_uint8* src = &window[startIdx]; + cc_uint8* dst = &window[curIdx]; + + for (i = 0; i < (len & ~0x3); i += 4) { + *dst++ = *src++; *dst++ = *src++; *dst++ = *src++; *dst++ = *src++; + } + for (; i < len; i++) { *dst++ = *src++; } + } else { + for (i = 0; i < len; i++) { + window[(curIdx + i) & INFLATE_WINDOW_MASK] = window[(startIdx + i) & INFLATE_WINDOW_MASK]; + } + } + curIdx = (curIdx + len) & INFLATE_WINDOW_MASK; + s->AvailOut -= len; copyLen += len; + } + } + + s->WindowIndex = curIdx; + if (!copyLen) return; + + if (copyStart + copyLen < INFLATE_WINDOW_SIZE) { + Mem_Copy(s->Output, &s->Window[copyStart], copyLen); + s->Output += copyLen; + } else { + partLen = INFLATE_WINDOW_SIZE - copyStart; + Mem_Copy(s->Output, &s->Window[copyStart], partLen); + s->Output += partLen; + Mem_Copy(s->Output, s->Window, copyLen - partLen); + s->Output += (copyLen - partLen); + } +} + +void Inflate_Process(struct InflateState* s) { + cc_uint32 len, dist, nlen; + cc_uint32 i, bits; + cc_uint32 blockHeader; + cc_result res; + + /* len/dist table variables */ + cc_uint32 distIdx, lenIdx; + int lit; + /* code lens table variables */ + cc_uint32 count, repeatCount; + cc_uint8 repeatValue; + /* window variables */ + cc_uint32 startIdx, curIdx; + cc_uint32 copyLen, windowCopyLen; + + for (;;) { + switch (s->State) { + case INFLATE_STATE_HEADER: { + Inflate_EnsureBits(s, 3); + blockHeader = Inflate_ReadBits(s, 3); + s->LastBlock = blockHeader & 1; + + switch (blockHeader >> 1) { + case 0: { /* Uncompressed block */ + Inflate_AlignBits(s); + s->State = INFLATE_STATE_UNCOMPRESSED_HEADER; + } break; + + case 1: { /* Fixed/static huffman compressed */ + (void)Huffman_Build(&s->Table.Lits, fixed_lits, INFLATE_MAX_LITS); + (void)Huffman_Build(&s->TableDists, fixed_dists, INFLATE_MAX_DISTS); + s->State = Inflate_NextCompressState(s); + } break; + + case 2: { /* Dynamic huffman compressed */ + s->State = INFLATE_STATE_DYNAMIC_HEADER; + } break; + + case 3: { + Inflate_Fail(s, INF_ERR_BLOCKTYPE); + } break; + + } + break; + } + + case INFLATE_STATE_UNCOMPRESSED_HEADER: { + Inflate_EnsureBits(s, 32); + len = Inflate_ReadBits(s, 16); + nlen = Inflate_ReadBits(s, 16); + + if (len != (nlen ^ 0xFFFFUL)) { + Inflate_Fail(s, INF_ERR_BLOCKTYPE); return; + } + s->Index = len; /* Reuse for 'uncompressed length' */ + s->State = INFLATE_STATE_UNCOMPRESSED_DATA; + } + + /* FALLTHRU */ + case INFLATE_STATE_UNCOMPRESSED_DATA: { + /* read bits left in bit buffer (slow way) */ + while (s->NumBits && s->AvailOut && s->Index) { + *s->Output = Inflate_ReadBits(s, 8); + s->Window[s->WindowIndex] = *s->Output; + + s->WindowIndex = (s->WindowIndex + 1) & INFLATE_WINDOW_MASK; + s->Output++; s->AvailOut--; s->Index--; + } + if (!s->AvailIn || !s->AvailOut) return; + + copyLen = min(s->AvailIn, s->AvailOut); + copyLen = min(copyLen, s->Index); + if (copyLen > 0) { + Mem_Copy(s->Output, s->NextIn, copyLen); + windowCopyLen = INFLATE_WINDOW_SIZE - s->WindowIndex; + windowCopyLen = min(windowCopyLen, copyLen); + + Mem_Copy(&s->Window[s->WindowIndex], s->Output, windowCopyLen); + /* Wrap around remainder of copy to start from beginning of window */ + if (windowCopyLen < copyLen) { + Mem_Copy(s->Window, &s->Output[windowCopyLen], copyLen - windowCopyLen); + } + + s->WindowIndex = (s->WindowIndex + copyLen) & INFLATE_WINDOW_MASK; + s->Output += copyLen; s->AvailOut -= copyLen; s->Index -= copyLen; + s->NextIn += copyLen; s->AvailIn -= copyLen; + } + + if (!s->Index) { s->State = Inflate_NextBlockState(s); } + break; + } + + case INFLATE_STATE_DYNAMIC_HEADER: { + Inflate_EnsureBits(s, 14); + s->NumLits = 257 + Inflate_ReadBits(s, 5); + s->NumDists = 1 + Inflate_ReadBits(s, 5); + s->NumCodeLens = 4 + Inflate_ReadBits(s, 4); + s->Index = 0; + s->State = INFLATE_STATE_DYNAMIC_CODELENS; + } + /* FALLTHRU */ + + case INFLATE_STATE_DYNAMIC_CODELENS: { + while (s->Index < s->NumCodeLens) { + Inflate_EnsureBits(s, 3); + i = codelens_order[s->Index]; + s->Buffer[i] = Inflate_ReadBits(s, 3); + s->Index++; + } + for (i = s->NumCodeLens; i < INFLATE_MAX_CODELENS; i++) { + s->Buffer[codelens_order[i]] = 0; + } + + s->Index = 0; + s->State = INFLATE_STATE_DYNAMIC_LITSDISTS; + res = Huffman_Build(&s->Table.CodeLens, s->Buffer, INFLATE_MAX_CODELENS); + if (res) { Inflate_Fail(s, res); return; } + } + + /* FALLTHRU */ + case INFLATE_STATE_DYNAMIC_LITSDISTS: { + count = s->NumLits + s->NumDists; + while (s->Index < count) { + int bits = Huffman_Decode(s, &s->Table.CodeLens); + if (bits < 16) { + if (bits == -1) return; + s->Buffer[s->Index] = (cc_uint8)bits; + s->Index++; + } else { + s->TmpCodeLens = bits; + s->State = INFLATE_STATE_DYNAMIC_LITSDISTSREPEAT; + break; + } + } + + if (s->Index == count) { + s->Index = 0; + s->State = Inflate_NextCompressState(s); + + res = Huffman_Build(&s->Table.Lits, s->Buffer, s->NumLits); + if (res) { Inflate_Fail(s, res); return; } + res = Huffman_Build(&s->TableDists, s->Buffer + s->NumLits, s->NumDists); + if (res) { Inflate_Fail(s, res); return; } + } + break; + } + + case INFLATE_STATE_DYNAMIC_LITSDISTSREPEAT: { + switch (s->TmpCodeLens) { + case 16: + Inflate_EnsureBits(s, 2); + repeatCount = Inflate_ReadBits(s, 2); + if (!s->Index) { Inflate_Fail(s, INF_ERR_REPEAT_BEG); return; } + repeatCount += 3; repeatValue = s->Buffer[s->Index - 1]; + break; + + case 17: + Inflate_EnsureBits(s, 3); + repeatCount = Inflate_ReadBits(s, 3); + repeatCount += 3; repeatValue = 0; + break; + + case 18: + Inflate_EnsureBits(s, 7); + repeatCount = Inflate_ReadBits(s, 7); + repeatCount += 11; repeatValue = 0; + break; + } + + count = s->NumLits + s->NumDists; + if (s->Index + repeatCount > count) { + Inflate_Fail(s, INF_ERR_REPEAT_END); return; + } + + Mem_Set(&s->Buffer[s->Index], repeatValue, repeatCount); + s->Index += repeatCount; + s->State = INFLATE_STATE_DYNAMIC_LITSDISTS; + break; + } + + case INFLATE_STATE_COMPRESSED_LIT: { + if (!s->AvailOut) return; + lit = Huffman_Decode(s, &s->Table.Lits); + + if (lit < 256) { + if (lit == -1) return; + *s->Output = (cc_uint8)lit; + s->Window[s->WindowIndex] = (cc_uint8)lit; + s->Output++; s->AvailOut--; + s->WindowIndex = (s->WindowIndex + 1) & INFLATE_WINDOW_MASK; + break; + } else if (lit == 256) { + s->State = Inflate_NextBlockState(s); + break; + } else { + s->TmpLit = lit - 257; + s->State = INFLATE_STATE_COMPRESSED_LITEXTRA; + } + } + + case INFLATE_STATE_COMPRESSED_LITEXTRA: { + lenIdx = s->TmpLit; + bits = len_bits[lenIdx]; + Inflate_EnsureBits(s, bits); + s->TmpLit = len_base[lenIdx] + Inflate_ReadBits(s, bits); + s->State = INFLATE_STATE_COMPRESSED_DIST; + } + + /* FALLTHRU */ + case INFLATE_STATE_COMPRESSED_DIST: { + s->TmpDist = Huffman_Decode(s, &s->TableDists); + if (s->TmpDist == -1) return; + s->State = INFLATE_STATE_COMPRESSED_DISTEXTRA; + } + + /* FALLTHRU */ + case INFLATE_STATE_COMPRESSED_DISTEXTRA: { + distIdx = s->TmpDist; + bits = dist_bits[distIdx]; + Inflate_EnsureBits(s, bits); + s->TmpDist = dist_base[distIdx] + Inflate_ReadBits(s, bits); + s->State = INFLATE_STATE_COMPRESSED_DATA; + } + + /* FALLTHRU */ + case INFLATE_STATE_COMPRESSED_DATA: { + if (!s->AvailOut) return; + len = s->TmpLit; dist = s->TmpDist; + len = min(len, s->AvailOut); + + /* TODO: Should we test outside of the loop, whether a masking will be required or not? */ + startIdx = (s->WindowIndex - dist) & INFLATE_WINDOW_MASK; + curIdx = s->WindowIndex; + for (i = 0; i < len; i++) { + cc_uint8 value = s->Window[(startIdx + i) & INFLATE_WINDOW_MASK]; + *s->Output = value; + s->Window[(curIdx + i) & INFLATE_WINDOW_MASK] = value; + s->Output++; + } + + s->WindowIndex = (curIdx + len) & INFLATE_WINDOW_MASK; + s->TmpLit -= len; + s->AvailOut -= len; + if (!s->TmpLit) { s->State = Inflate_NextCompressState(s); } + break; + } + + case INFLATE_STATE_FASTCOMPRESSED: { + Inflate_InflateFast(s); + if (s->State == INFLATE_STATE_FASTCOMPRESSED) { + s->State = Inflate_NextCompressState(s); + } + break; + } + + case INFLATE_STATE_DONE: + return; + } + } +} + +static cc_result Inflate_StreamRead(struct Stream* stream, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + struct InflateState* state; + cc_uint8* inputEnd; + cc_uint32 read, left; + cc_uint32 startAvailOut; + cc_bool hasInput; + cc_result res; + + *modified = 0; + state = (struct InflateState*)stream->meta.inflate; + state->Output = data; + state->AvailOut = count; + + hasInput = true; + while (state->AvailOut > 0 && hasInput) { + if (state->State == INFLATE_STATE_DONE) return state->result; + + if (!state->AvailIn) { + /* Fully used up input buffer. Cycle back to start. */ + inputEnd = state->Input + INFLATE_MAX_INPUT; + if (state->NextIn == inputEnd) state->NextIn = state->Input; + + left = (cc_uint32)(inputEnd - state->NextIn); + res = state->Source->Read(state->Source, state->NextIn, left, &read); + if (res) return res; + + /* Did we fail to read in more input data? Can't immediately return here, */ + /* because there might be a few bits of data left in the bit buffer */ + hasInput = read > 0; + state->AvailIn += read; + } + + /* Reading data reduces available out */ + startAvailOut = state->AvailOut; + Inflate_Process(state); + *modified += (startAvailOut - state->AvailOut); + } + return 0; +} + +void Inflate_MakeStream2(struct Stream* stream, struct InflateState* state, struct Stream* underlying) { + Stream_Init(stream); + Inflate_Init2(state, underlying); + stream->meta.inflate = state; + stream->Read = Inflate_StreamRead; +} + + +/*########################################################################################################################* +*---------------------------------------------------Deflate (compress)----------------------------------------------------* +*#########################################################################################################################*/ +/* these are copies of len_base and dist_base, with UINT16_MAX instead of 0 for sentinel cutoff */ +static const cc_uint16 deflate_len[30] = { + 3,4,5,6,7,8,9,10,11,13, + 15,17,19,23,27,31,35,43,51,59, + 67,83,99,115,131,163,195,227,258,UInt16_MaxValue +}; +static const cc_uint16 deflate_dist[31] = { + 1,2,3,4,5,7,9,13,17,25, + 33,49,65,97,129,193,257,385,513,769, + 1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,UInt16_MaxValue +}; + +/* Pushes given bits, but does not write them */ +#define Deflate_PushBits(state, value, bits) state->Bits |= (value) << state->NumBits; state->NumBits += (bits); +/* Pushes bits of the huffman codeword bits for the given literal, but does not write them */ +#define Deflate_PushLit(state, value) Deflate_PushBits(state, state->LitsCodewords[value], state->LitsLens[value]) +/* Pushes given bits (reversing for huffman code), but does not write them */ +#define Deflate_PushHuff(state, value, bits) Deflate_PushBits(state, Huffman_ReverseBits(value, bits), bits) +/* Writes given byte to output */ +#define Deflate_WriteByte(state) *state->NextOut++ = state->Bits; state->AvailOut--; state->Bits >>= 8; state->NumBits -= 8; +/* Flushes bits in buffer to output buffer */ +#define Deflate_FlushBits(state) while (state->NumBits >= 8) { Deflate_WriteByte(state); } + +#define MIN_MATCH_LEN 3 +#define MAX_MATCH_LEN 258 + +/* Number of bytes that match (are the same) from a and b */ +static int Deflate_MatchLen(cc_uint8* a, cc_uint8* b, int maxLen) { + int i = 0; + while (i < maxLen && *a == *b) { i++; a++; b++; } + return i; +} + +/* Hashes 3 bytes of data */ +static cc_uint32 Deflate_Hash(cc_uint8* src) { + return (cc_uint32)((src[0] << 8) ^ (src[1] << 4) ^ (src[2])) & DEFLATE_HASH_MASK; +} + +/* Writes a literal to state->Output */ +static void Deflate_Lit(struct DeflateState* state, int lit) { + Deflate_PushLit(state, lit); + Deflate_FlushBits(state); +} + +/* Writes a length-distance pair to state->Output */ +static void Deflate_LenDist(struct DeflateState* state, int len, int dist) { + int j; + /* TODO: Do we actually need the if (len_bits[j]) ????????? does writing 0 bits matter??? */ + + for (j = 0; len >= deflate_len[j + 1]; j++); + Deflate_PushLit(state, j + 257); + if (len_bits[j]) { Deflate_PushBits(state, len - deflate_len[j], len_bits[j]); } + Deflate_FlushBits(state); + + for (j = 0; dist >= deflate_dist[j + 1]; j++); + Deflate_PushHuff(state, j, 5); + if (dist_bits[j]) { Deflate_PushBits(state, dist - deflate_dist[j], dist_bits[j]); } + Deflate_FlushBits(state); +} + +/* Moves "current block" to "previous block", adjusting state if needed. */ +static void Deflate_MoveBlock(struct DeflateState* state) { + int i; + Mem_Copy(state->Input, state->Input + DEFLATE_BLOCK_SIZE, DEFLATE_BLOCK_SIZE); + state->InputPosition = DEFLATE_BLOCK_SIZE; + + /* adjust hash table offsets, removing offsets that are no longer in data at all */ + for (i = 0; i < Array_Elems(state->Head); i++) { + state->Head[i] = state->Head[i] < DEFLATE_BLOCK_SIZE ? 0 : (state->Head[i] - DEFLATE_BLOCK_SIZE); + } + for (i = 0; i < Array_Elems(state->Prev); i++) { + state->Prev[i] = state->Prev[i] < DEFLATE_BLOCK_SIZE ? 0 : (state->Prev[i] - DEFLATE_BLOCK_SIZE); + } +} + +/* Compresses current block of data */ +static cc_result Deflate_FlushBlock(struct DeflateState* state, int len) { + cc_uint32 hash, nextHash; + int bestLen, maxLen, matchLen, depth; + int bestPos, pos, nextPos; + cc_uint16 oldHead; + cc_uint8* input; + cc_uint8* cur; + cc_result res; + + if (!state->WroteHeader) { + state->WroteHeader = true; + Deflate_PushBits(state, 3, 3); /* final block TRUE, block type FIXED */ + } + + /* Based off descriptions from http://www.gzip.org/algorithm.txt and + https://github.com/nothings/stb/blob/master/stb_image_write.h */ + input = state->Input; + cur = input + DEFLATE_BLOCK_SIZE; + + /* Compress current block of data */ + /* Use > instead of >=, because also try match at one byte after current */ + while (len > MIN_MATCH_LEN) { + hash = Deflate_Hash(cur); + maxLen = min(len, MAX_MATCH_LEN); + + bestLen = MIN_MATCH_LEN - 1; /* Match must be at least 3 bytes */ + bestPos = 0; + + /* Find longest match starting at this byte */ + /* Only explore up to 5 previous matches, to avoid slow performance */ + /* (i.e prefer quickly saving maps/screenshots to completely optimal filesize) */ + pos = state->Head[hash]; + for (depth = 0; pos != 0 && depth < 5; depth++) { + matchLen = Deflate_MatchLen(&input[pos], cur, maxLen); + if (matchLen > bestLen) { bestLen = matchLen; bestPos = pos; } + pos = state->Prev[pos]; + } + + /* Insert this entry into the hash chain */ + pos = (int)(cur - input); + oldHead = state->Head[hash]; + state->Head[hash] = pos; + state->Prev[pos] = oldHead; + + /* Lazy evaluation: Find longest match starting at next byte */ + /* If that's longer than the longest match at current byte, throwaway this match */ + if (bestPos) { + nextHash = Deflate_Hash(cur + 1); + nextPos = state->Head[nextHash]; + maxLen = min(len - 1, MAX_MATCH_LEN); + + for (depth = 0; nextPos != 0 && depth < 5; depth++) { + matchLen = Deflate_MatchLen(&input[nextPos], cur + 1, maxLen); + if (matchLen > bestLen) { bestPos = 0; break; } + nextPos = state->Prev[nextPos]; + } + } + + if (bestPos) { + Deflate_LenDist(state, bestLen, pos - bestPos); + len -= bestLen; cur += bestLen; + } else { + Deflate_Lit(state, *cur); + len--; cur++; + } + + /* leave room for a few bytes and literals at end */ + if (state->AvailOut >= 20) continue; + res = Stream_Write(state->Dest, state->Output, DEFLATE_OUT_SIZE - state->AvailOut); + state->NextOut = state->Output; + state->AvailOut = DEFLATE_OUT_SIZE; + if (res) return res; + } + + /* literals for last few bytes */ + while (len > 0) { + Deflate_Lit(state, *cur); + len--; cur++; + } + + res = Stream_Write(state->Dest, state->Output, DEFLATE_OUT_SIZE - state->AvailOut); + state->NextOut = state->Output; + state->AvailOut = DEFLATE_OUT_SIZE; + + Deflate_MoveBlock(state); + return res; +} + +/* Adds data to buffered output data, flushing if needed */ +static cc_result Deflate_StreamWrite(struct Stream* stream, const cc_uint8* data, cc_uint32 total, cc_uint32* modified) { + struct DeflateState* state; + cc_result res; + + state = (struct DeflateState*)stream->meta.inflate; + *modified = 0; + + while (total > 0) { + cc_uint8* dst = &state->Input[state->InputPosition]; + cc_uint32 len = total; + if (state->InputPosition + len >= DEFLATE_BUFFER_SIZE) { + len = DEFLATE_BUFFER_SIZE - state->InputPosition; + } + + Mem_Copy(dst, data, len); + total -= len; + state->InputPosition += len; + *modified += len; + data += len; + + if (state->InputPosition == DEFLATE_BUFFER_SIZE) { + res = Deflate_FlushBlock(state, DEFLATE_BLOCK_SIZE); + if (res) return res; + } + } + return 0; +} + +/* Flushes any buffered data, then writes terminating symbol */ +static cc_result Deflate_StreamClose(struct Stream* stream) { + struct DeflateState* state; + cc_result res; + + state = (struct DeflateState*)stream->meta.inflate; + res = Deflate_FlushBlock(state, state->InputPosition - DEFLATE_BLOCK_SIZE); + if (res) return res; + + /* Write huffman encoded "literal 256" to terminate symbols */ + Deflate_PushLit(state, 256); + Deflate_FlushBits(state); + + /* In case last byte still has a few extra bits */ + if (state->NumBits) { + while (state->NumBits < 8) { Deflate_PushBits(state, 0, 1); } + Deflate_FlushBits(state); + } + + return Stream_Write(state->Dest, state->Output, DEFLATE_OUT_SIZE - state->AvailOut); +} + +/* Constructs a huffman encoding table (for values to codewords) */ +static void Deflate_BuildTable(const cc_uint8* lens, int count, cc_uint16* codewords, cc_uint8* bitlens) { + int i, j, offset, codeword; + struct HuffmanTable table; + + /* NOTE: Can ignore since lens table is not user controlled */ + (void)Huffman_Build(&table, lens, count); + for (i = 0; i < INFLATE_MAX_BITS; i++) { + if (!table.endCodewords[i]) continue; + count = table.endCodewords[i] - table.firstCodewords[i]; + + for (j = 0; j < count; j++) { + offset = table.values[table.firstOffsets[i] + j]; + codeword = table.firstCodewords[i] + j; + bitlens[offset] = i; + codewords[offset] = Huffman_ReverseBits(codeword, i); + } + } +} + +void Deflate_MakeStream(struct Stream* stream, struct DeflateState* state, struct Stream* underlying) { + Stream_Init(stream); + stream->meta.inflate = state; + stream->Write = Deflate_StreamWrite; + stream->Close = Deflate_StreamClose; + + /* First half of buffer is "previous block" */ + state->InputPosition = DEFLATE_BLOCK_SIZE; + state->Bits = 0; + state->NumBits = 0; + + state->NextOut = state->Output; + state->AvailOut = DEFLATE_OUT_SIZE; + state->Dest = underlying; + state->WroteHeader = false; + + Mem_Set(state->Head, 0, sizeof(state->Head)); + Mem_Set(state->Prev, 0, sizeof(state->Prev)); + Deflate_BuildTable(fixed_lits, INFLATE_MAX_LITS, state->LitsCodewords, state->LitsLens); +} + + +/*########################################################################################################################* +*-----------------------------------------------------GZip (compress)-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GZip_StreamClose(struct Stream* stream) { + struct GZipState* state = (struct GZipState*)stream->meta.inflate; + cc_uint8 data[8]; + cc_result res; + + if ((res = Deflate_StreamClose(stream))) return res; + Stream_SetU32_LE(&data[0], state->Crc32 ^ 0xFFFFFFFFUL); + Stream_SetU32_LE(&data[4], state->Size); + return Stream_Write(state->Base.Dest, data, sizeof(data)); +} + +static cc_result GZip_StreamWrite(struct Stream* stream, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + struct GZipState* state = (struct GZipState*)stream->meta.inflate; + cc_uint32 i, crc32 = state->Crc32; + state->Size += count; + + /* TODO: Optimise this calculation */ + for (i = 0; i < count; i++) { + crc32 = Utils_Crc32Table[(crc32 ^ data[i]) & 0xFF] ^ (crc32 >> 8); + } + + state->Crc32 = crc32; + return Deflate_StreamWrite(stream, data, count, modified); +} + +static cc_result GZip_StreamWriteFirst(struct Stream* stream, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + static cc_uint8 header[10] = { 0x1F, 0x8B, 0x08 }; /* GZip header */ + struct GZipState* state = (struct GZipState*)stream->meta.inflate; + cc_result res; + + if ((res = Stream_Write(state->Base.Dest, header, sizeof(header)))) return res; + stream->Write = GZip_StreamWrite; + return GZip_StreamWrite(stream, data, count, modified); +} + +void GZip_MakeStream(struct Stream* stream, struct GZipState* state, struct Stream* underlying) { + Deflate_MakeStream(stream, &state->Base, underlying); + state->Crc32 = 0xFFFFFFFFUL; + state->Size = 0; + stream->Write = GZip_StreamWriteFirst; + stream->Close = GZip_StreamClose; +} + + +/*########################################################################################################################* +*-----------------------------------------------------ZLib (compress)-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_result ZLib_StreamClose(struct Stream* stream) { + struct ZLibState* state = (struct ZLibState*)stream->meta.inflate; + cc_uint8 data[4]; + cc_result res; + + if ((res = Deflate_StreamClose(stream))) return res; + Stream_SetU32_BE(&data[0], state->Adler32); + return Stream_Write(state->Base.Dest, data, sizeof(data)); +} + +static cc_result ZLib_StreamWrite(struct Stream* stream, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + struct ZLibState* state = (struct ZLibState*)stream->meta.inflate; + cc_uint32 i, adler32 = state->Adler32; + cc_uint32 s1 = adler32 & 0xFFFF, s2 = (adler32 >> 16) & 0xFFFF; + + /* TODO: Optimise this calculation */ + for (i = 0; i < count; i++) { + #define ADLER32_BASE 65521 + s1 = (s1 + data[i]) % ADLER32_BASE; + s2 = (s2 + s1) % ADLER32_BASE; + } + + state->Adler32 = (s2 << 16) | s1; + return Deflate_StreamWrite(stream, data, count, modified); +} + +static cc_result ZLib_StreamWriteFirst(struct Stream* stream, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + static cc_uint8 header[2] = { 0x78, 0x9C }; /* ZLib header */ + struct ZLibState* state = (struct ZLibState*)stream->meta.inflate; + cc_result res; + + if ((res = Stream_Write(state->Base.Dest, header, sizeof(header)))) return res; + stream->Write = ZLib_StreamWrite; + return ZLib_StreamWrite(stream, data, count, modified); +} + +void ZLib_MakeStream(struct Stream* stream, struct ZLibState* state, struct Stream* underlying) { + Deflate_MakeStream(stream, &state->Base, underlying); + state->Adler32 = 1; + stream->Write = ZLib_StreamWriteFirst; + stream->Close = ZLib_StreamClose; +} + + +/*########################################################################################################################* +*--------------------------------------------------------ZipReader--------------------------------------------------------* +*#########################################################################################################################*/ +#define ZIP_MAXNAMELEN 512 +#define ZIP_MAX_ENTRIES 1024 + +/* Stores state for reading and processing entries in a .zip archive */ +struct ZipState { + struct Stream* source; + Zip_SelectEntry SelectEntry; + Zip_ProcessEntry ProcessEntry; + + /* Number of entries selected by SelectEntry */ + int usedEntries; + /* Total number of entries in the archive */ + int totalEntries; + /* Offset to central directory entries */ + cc_uint32 centralDirBeg; + /* Data for each entry in the .zip archive */ + struct ZipEntry entries[ZIP_MAX_ENTRIES]; +}; + +static cc_result Zip_ReadLocalFileHeader(struct ZipState* state, struct ZipEntry* entry) { + struct Stream* stream = state->source; + cc_uint8 header[26]; + cc_string path; char pathBuffer[ZIP_MAXNAMELEN]; + cc_uint32 compressedSize, uncompressedSize; + int method, pathLen, extraLen; + struct Stream portion, compStream; + struct InflateState inflate; + cc_result res; + + if ((res = Stream_Read(stream, header, sizeof(header)))) return res; + pathLen = Stream_GetU16_LE(&header[22]); + if (pathLen > ZIP_MAXNAMELEN) return ZIP_ERR_FILENAME_LEN; + + /* NOTE: ZIP spec says path uses code page 437 for encoding */ + path = String_Init(pathBuffer, pathLen, pathLen); + if ((res = Stream_Read(stream, (cc_uint8*)pathBuffer, pathLen))) return res; + if (!state->SelectEntry(&path)) return 0; + + extraLen = Stream_GetU16_LE(&header[24]); + /* local file may have extra data before actual data (e.g. ZIP64) */ + if ((res = stream->Skip(stream, extraLen))) return res; + + method = Stream_GetU16_LE(&header[4]); + compressedSize = Stream_GetU32_LE(&header[14]); + uncompressedSize = Stream_GetU32_LE(&header[18]); + + /* Some .zip files don't set these in local file header */ + if (!compressedSize) compressedSize = entry->CompressedSize; + if (!uncompressedSize) uncompressedSize = entry->UncompressedSize; + + if (method == 0) { + Stream_ReadonlyPortion(&portion, stream, uncompressedSize); + return state->ProcessEntry(&path, &portion, entry); + } else if (method == 8) { + Stream_ReadonlyPortion(&portion, stream, compressedSize); + Inflate_MakeStream2(&compStream, &inflate, &portion); + return state->ProcessEntry(&path, &compStream, entry); + } else { + Platform_Log1("Unsupported.zip entry compression method: %i", &method); + /* TODO: Should this be an error */ + } + return 0; +} + +static cc_result Zip_ReadCentralDirectory(struct ZipState* state) { + struct Stream* stream = state->source; + struct ZipEntry* entry; + cc_uint8 header[42]; + + cc_string path; char pathBuffer[ZIP_MAXNAMELEN]; + int pathLen, extraLen, commentLen; + cc_result res; + + if ((res = Stream_Read(stream, header, sizeof(header)))) return res; + pathLen = Stream_GetU16_LE(&header[24]); + if (pathLen > ZIP_MAXNAMELEN) return ZIP_ERR_FILENAME_LEN; + + /* NOTE: ZIP spec says path uses code page 437 for encoding */ + path = String_Init(pathBuffer, pathLen, pathLen); + if ((res = Stream_Read(stream, (cc_uint8*)pathBuffer, pathLen))) return res; + + /* skip data following central directory entry header */ + extraLen = Stream_GetU16_LE(&header[26]); + commentLen = Stream_GetU16_LE(&header[28]); + if ((res = stream->Skip(stream, extraLen + commentLen))) return res; + + if (!state->SelectEntry(&path)) return 0; + if (state->usedEntries >= ZIP_MAX_ENTRIES) return ZIP_ERR_TOO_MANY_ENTRIES; + entry = &state->entries[state->usedEntries++]; + + entry->CompressedSize = Stream_GetU32_LE(&header[16]); + entry->UncompressedSize = Stream_GetU32_LE(&header[20]); + entry->LocalHeaderOffset = Stream_GetU32_LE(&header[38]); + return 0; +} + +static cc_result Zip_ReadEndOfCentralDirectory(struct ZipState* state) { + struct Stream* stream = state->source; + cc_uint8 header[18]; + + cc_result res; + if ((res = Stream_Read(stream, header, sizeof(header)))) return res; + + state->totalEntries = Stream_GetU16_LE(&header[6]); + state->centralDirBeg = Stream_GetU32_LE(&header[12]); + return 0; +} + +enum ZipSig { + ZIP_SIG_ENDOFCENTRALDIR = 0x06054b50, + ZIP_SIG_CENTRALDIR = 0x02014b50, + ZIP_SIG_LOCALFILEHEADER = 0x04034b50 +}; + +cc_result Zip_Extract(struct Stream* source, Zip_SelectEntry selector, Zip_ProcessEntry processor) { + struct ZipState state; + cc_uint32 stream_len; + cc_uint32 sig = 0; + int i, count; + + cc_result res; + if ((res = source->Length(source, &stream_len))) return res; + + /* At -22 for nearly all zips, but try a bit further back in case of comment */ + count = min(257, stream_len); + for (i = 22; i < count; i++) { + res = source->Seek(source, stream_len - i); + if (res) return ZIP_ERR_SEEK_END_OF_CENTRAL_DIR; + + if ((res = Stream_ReadU32_LE(source, &sig))) return res; + if (sig == ZIP_SIG_ENDOFCENTRALDIR) break; + } + + state.source = source; + state.SelectEntry = selector; + state.ProcessEntry = processor; + + if (sig != ZIP_SIG_ENDOFCENTRALDIR) return ZIP_ERR_NO_END_OF_CENTRAL_DIR; + res = Zip_ReadEndOfCentralDirectory(&state); + if (res) return res; + + res = source->Seek(source, state.centralDirBeg); + if (res) return ZIP_ERR_SEEK_CENTRAL_DIR; + state.usedEntries = 0; + + /* Read all the central directory entries */ + for (i = 0; i < state.totalEntries; i++) { + if ((res = Stream_ReadU32_LE(source, &sig))) return res; + + if (sig == ZIP_SIG_CENTRALDIR) { + res = Zip_ReadCentralDirectory(&state); + if (res) return res; + } else if (sig == ZIP_SIG_ENDOFCENTRALDIR) { + break; + } else { + return ZIP_ERR_INVALID_CENTRAL_DIR; + } + } + + /* Now read the local file header entries */ + for (i = 0; i < state.usedEntries; i++) { + struct ZipEntry* entry = &state.entries[i]; + res = source->Seek(source, entry->LocalHeaderOffset); + if (res) return ZIP_ERR_SEEK_LOCAL_DIR; + + if ((res = Stream_ReadU32_LE(source, &sig))) return res; + if (sig != ZIP_SIG_LOCALFILEHEADER) return ZIP_ERR_INVALID_LOCAL_DIR; + + res = Zip_ReadLocalFileHeader(&state, entry); + if (res) return res; + } + return 0; +} diff --git a/src/Deflate.h b/src/Deflate.h new file mode 100644 index 0000000..d3dd490 --- /dev/null +++ b/src/Deflate.h @@ -0,0 +1,137 @@ +#ifndef CC_DEFLATE_H +#define CC_DEFLATE_H +#include "Core.h" +/* Decodes data compressed using DEFLATE in a streaming manner. + Partially based off information from + https://handmade.network/forums/wip/t/2363-implementing_a_basic_png_reader_the_handmade_way + http://commandlinefanatic.com/cgi-bin/showarticle.cgi?article=art001 + https://www.ietf.org/rfc/rfc1951.txt + https://github.com/nothings/stb/blob/master/stb_image.h + https://www.hanshq.net/zip.html + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Stream; + +struct GZipHeader { cc_uint8 state; cc_bool done; cc_uint8 partsRead; int flags; }; +void GZipHeader_Init(struct GZipHeader* header); +cc_result GZipHeader_Read(struct Stream* s, struct GZipHeader* header); + +struct ZLibHeader { cc_uint8 state; cc_bool done; }; +void ZLibHeader_Init(struct ZLibHeader* header); +cc_result ZLibHeader_Read(struct Stream* s, struct ZLibHeader* header); + + +#define INFLATE_MAX_INPUT 8192 +#define INFLATE_MAX_CODELENS 19 +#define INFLATE_MAX_LITS 288 +#define INFLATE_MAX_DISTS 32 +#define INFLATE_MAX_LITS_DISTS (INFLATE_MAX_LITS + INFLATE_MAX_DISTS) +#define INFLATE_MAX_BITS 16 + +#define INFLATE_FAST_BITS 9 +#define INFLATE_FAST_LEN_SHIFT 9 +#define INFLATE_FAST_VAL_MASK 0x1FF + +#define INFLATE_WINDOW_SIZE 0x8000UL +#define INFLATE_WINDOW_MASK 0x7FFFUL + +struct HuffmanTable { + cc_int16 fast[1 << INFLATE_FAST_BITS]; /* Fast lookup table for huffman codes */ + cc_uint16 firstCodewords[INFLATE_MAX_BITS]; /* Starting codeword for each bit length */ + cc_uint16 endCodewords[INFLATE_MAX_BITS]; /* (Last codeword + 1) for each bit length. 0 is ignored. */ + cc_uint16 firstOffsets[INFLATE_MAX_BITS]; /* Base offset into Values for codewords of each bit length. */ + cc_uint16 values[INFLATE_MAX_LITS]; /* Values/Symbols list */ +}; + +struct InflateState { + cc_uint8 State; + cc_bool LastBlock; /* Whether the last DEFLATE block has been encounted in the stream */ + cc_uint32 Bits; /* Holds bits across byte boundaries */ + cc_uint32 NumBits; /* Number of bits in Bits buffer */ + + cc_uint8* NextIn; /* Pointer within Input buffer to next byte that can be read */ + cc_uint32 AvailIn; /* Max number of bytes that can be read from Input buffer */ + cc_uint8* Output; /* Pointer for output data */ + cc_uint32 AvailOut; /* Max number of bytes that can be written to Output buffer */ + struct Stream* Source; /* Source for filling Input buffer */ + + cc_uint32 Index; /* General purpose index / counter */ + cc_uint32 WindowIndex; /* Current index within window circular buffer */ + cc_uint32 NumCodeLens, NumLits, NumDists; /* Temp counters */ + cc_uint32 TmpCodeLens, TmpLit, TmpDist; /* Temp huffman codes */ + + cc_uint8 Input[INFLATE_MAX_INPUT]; /* Buffer for input to DEFLATE */ + cc_uint8 Buffer[INFLATE_MAX_LITS_DISTS]; /* General purpose temp array */ + union { + struct HuffmanTable CodeLens; /* Values represent codeword lengths of lits/dists codewords */ + struct HuffmanTable Lits; /* Values represent literal or lengths */ + } Table; /* union to save on memory */ + struct HuffmanTable TableDists; /* Values represent distances back */ + cc_uint8 Window[INFLATE_WINDOW_SIZE]; /* Holds circular buffer of recent output data, used for LZ77 */ + cc_result result; +}; + +/* Initialises DEFLATE decompressor state to defaults. */ +CC_API void Inflate_Init2(struct InflateState* state, struct Stream* source); +/* Attempts to decompress as much of the currently pending data as possible. */ +/* NOTE: This is a low level call - usually you treat as a stream via Inflate_MakeStream. */ +void Inflate_Process(struct InflateState* s); +/* Deompresses input data read from another stream using DEFLATE. Read only stream. */ +/* NOTE: This only uncompresses pure DEFLATE compressed data. */ +/* If data starts with a GZIP or ZLIB header, use GZipHeader_Read or ZLibHeader_Read to first skip it. */ +CC_API void Inflate_MakeStream2(struct Stream* stream, struct InflateState* state, struct Stream* underlying); + + +#define DEFLATE_BLOCK_SIZE 16384 +#define DEFLATE_BUFFER_SIZE 32768 +#define DEFLATE_OUT_SIZE 8192 +#define DEFLATE_HASH_SIZE 0x1000UL +#define DEFLATE_HASH_MASK 0x0FFFUL +struct DeflateState { + cc_uint32 Bits; /* Holds bits across byte boundaries */ + cc_uint32 NumBits; /* Number of bits in Bits buffer */ + cc_uint32 InputPosition; + + cc_uint8* NextOut; /* Pointer within Output buffer to next byte that can be written */ + cc_uint32 AvailOut; /* Max number of bytes that can be written to Output buffer */ + struct Stream* Dest; /* Destination that Output buffer is written to */ + + cc_uint16 LitsCodewords[INFLATE_MAX_LITS]; /* Codewords for each value */ + cc_uint8 LitsLens[INFLATE_MAX_LITS]; /* Bit lengths of each codeword */ + + cc_uint8 Input[DEFLATE_BUFFER_SIZE]; + cc_uint8 Output[DEFLATE_OUT_SIZE]; + cc_uint16 Head[DEFLATE_HASH_SIZE]; + cc_uint16 Prev[DEFLATE_BUFFER_SIZE]; + /* NOTE: The largest possible value that can get */ + /* stored in Head/Prev is <= DEFLATE_BUFFER_SIZE */ + cc_bool WroteHeader; +}; +/* Compresses input data using DEFLATE, then writes compressed output to another stream. Write only stream. */ +/* DEFLATE compression is pure compressed data, there is no header or footer. */ +CC_API void Deflate_MakeStream(struct Stream* stream, struct DeflateState* state, struct Stream* underlying); + +struct GZipState { struct DeflateState Base; cc_uint32 Crc32, Size; }; +/* Compresses input data using GZIP, then writes compressed output to another stream. Write only stream. */ +/* GZIP compression is GZIP header, followed by DEFLATE compressed data, followed by GZIP footer. */ +CC_API void GZip_MakeStream( struct Stream* stream, struct GZipState* state, struct Stream* underlying); +typedef void (*FP_GZip_MakeStream)(struct Stream* stream, struct GZipState* state, struct Stream* underlying); + +struct ZLibState { struct DeflateState Base; cc_uint32 Adler32; }; +/* Compresses input data using ZLIB, then writes compressed output to another stream. Write only stream. */ +/* ZLIB compression is ZLIB header, followed by DEFLATE compressed data, followed by ZLIB footer. */ +CC_API void ZLib_MakeStream( struct Stream* stream, struct ZLibState* state, struct Stream* underlying); +typedef void (*FP_ZLib_MakeStream)(struct Stream* stream, struct ZLibState* state, struct Stream* underlying); + +/* Minimal data needed to describe an entry in a .zip archive */ +struct ZipEntry { cc_uint32 CompressedSize, UncompressedSize, LocalHeaderOffset; }; +/* Callback function to process the data in a .zip archive entry */ +/* Return non-zero to indicate an error and stop further processing */ +/* NOTE: data stream MAY NOT be seekable (i.e. entry data might be compressed) */ +typedef cc_result (*Zip_ProcessEntry)(const cc_string* path, struct Stream* data, struct ZipEntry* entry); +/* Predicate used to select which entries in a .zip archive get processed */ +/* NOTE: returning false entirely skips the entry (avoids pointless seek to entry) */ +typedef cc_bool (*Zip_SelectEntry)(const cc_string* path); + +CC_API cc_result Zip_Extract(struct Stream* source, Zip_SelectEntry selector, Zip_ProcessEntry processor); +#endif diff --git a/src/Drawer.c b/src/Drawer.c new file mode 100644 index 0000000..527a5a1 --- /dev/null +++ b/src/Drawer.c @@ -0,0 +1,137 @@ +#include "Drawer.h" +#include "TexturePack.h" +#include "Constants.h" +#include "Graphics.h" +struct _DrawerData Drawer; + +void Drawer_XMin(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices) { + struct VertexTextured* v = *vertices; + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = Drawer.MinBB.z; + float u2 = (count - 1) + Drawer.MaxBB.z * UV2_Scale; + float v1 = vOrigin + Drawer.MaxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + Drawer.MinBB.y * Atlas1D.InvTileSize * UV2_Scale; + + float x1 = Drawer.X1; + float y1 = Drawer.Y1, y2 = Drawer.Y2; + float z1 = Drawer.Z1, z2 = Drawer.Z2 + (count - 1); + + if (Drawer.Tinted) col = PackedCol_Tint(col, Drawer.TintCol); + + v->x = x1; v->y = y2; v->z = z2; v->Col = col; v->U = u2; v->V = v1; v++; + v->x = x1; v->y = y2; v->z = z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x1; v->y = y1; v->z = z1; v->Col = col; v->U = u1; v->V = v2; v++; + v->x = x1; v->y = y1; v->z = z2; v->Col = col; v->U = u2; v->V = v2; v++; + *vertices = v; +} + +void Drawer_XMax(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices) { + struct VertexTextured* v = *vertices; + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = (count - Drawer.MinBB.z); + float u2 = (1 - Drawer.MaxBB.z) * UV2_Scale; + float v1 = vOrigin + Drawer.MaxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + Drawer.MinBB.y * Atlas1D.InvTileSize * UV2_Scale; + + float x2 = Drawer.X2; + float y1 = Drawer.Y1, y2 = Drawer.Y2; + float z1 = Drawer.Z1, z2 = Drawer.Z2 + (count - 1); + + if (Drawer.Tinted) col = PackedCol_Tint(col, Drawer.TintCol); + + v->x = x2; v->y = y2; v->z = z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x2; v->y = y2; v->z = z2; v->Col = col; v->U = u2; v->V = v1; v++; + v->x = x2; v->y = y1; v->z = z2; v->Col = col; v->U = u2; v->V = v2; v++; + v->x = x2; v->y = y1; v->z = z1; v->Col = col; v->U = u1; v->V = v2; v++; + *vertices = v; +} + +void Drawer_ZMin(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices) { + struct VertexTextured* v = *vertices; + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = (count - Drawer.MinBB.x); + float u2 = (1 - Drawer.MaxBB.x) * UV2_Scale; + float v1 = vOrigin + Drawer.MaxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + Drawer.MinBB.y * Atlas1D.InvTileSize * UV2_Scale; + + float x1 = Drawer.X1, x2 = Drawer.X2 + (count - 1); + float y1 = Drawer.Y1, y2 = Drawer.Y2; + float z1 = Drawer.Z1; + + if (Drawer.Tinted) col = PackedCol_Tint(col, Drawer.TintCol); + + v->x = x2; v->y = y1; v->z = z1; v->Col = col; v->U = u2; v->V = v2; v++; + v->x = x1; v->y = y1; v->z = z1; v->Col = col; v->U = u1; v->V = v2; v++; + v->x = x1; v->y = y2; v->z = z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x2; v->y = y2; v->z = z1; v->Col = col; v->U = u2; v->V = v1; v++; + *vertices = v; +} + +void Drawer_ZMax(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices) { + struct VertexTextured* v = *vertices; + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = Drawer.MinBB.x; + float u2 = (count - 1) + Drawer.MaxBB.x * UV2_Scale; + float v1 = vOrigin + Drawer.MaxBB.y * Atlas1D.InvTileSize; + float v2 = vOrigin + Drawer.MinBB.y * Atlas1D.InvTileSize * UV2_Scale; + + float x1 = Drawer.X1, x2 = Drawer.X2 + (count - 1); + float y1 = Drawer.Y1, y2 = Drawer.Y2; + float z2 = Drawer.Z2; + + if (Drawer.Tinted) col = PackedCol_Tint(col, Drawer.TintCol); + + v->x = x2; v->y = y2; v->z = z2; v->Col = col; v->U = u2; v->V = v1; v++; + v->x = x1; v->y = y2; v->z = z2; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x1; v->y = y1; v->z = z2; v->Col = col; v->U = u1; v->V = v2; v++; + v->x = x2; v->y = y1; v->z = z2; v->Col = col; v->U = u2; v->V = v2; v++; + *vertices = v; +} + +void Drawer_YMin(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices) { + struct VertexTextured* v = *vertices; + + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + float u1 = Drawer.MinBB.x; + float u2 = (count - 1) + Drawer.MaxBB.x * UV2_Scale; + float v1 = vOrigin + Drawer.MinBB.z * Atlas1D.InvTileSize; + float v2 = vOrigin + Drawer.MaxBB.z * Atlas1D.InvTileSize * UV2_Scale; + + float x1 = Drawer.X1, x2 = Drawer.X2 + (count - 1); + float y1 = Drawer.Y1; + float z1 = Drawer.Z1, z2 = Drawer.Z2; + + if (Drawer.Tinted) col = PackedCol_Tint(col, Drawer.TintCol); + + v->x = x2; v->y = y1; v->z = z2; v->Col = col; v->U = u2; v->V = v2; v++; + v->x = x1; v->y = y1; v->z = z2; v->Col = col; v->U = u1; v->V = v2; v++; + v->x = x1; v->y = y1; v->z = z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x2; v->y = y1; v->z = z1; v->Col = col; v->U = u2; v->V = v1; v++; + *vertices = v; +} + +void Drawer_YMax(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices) { + struct VertexTextured* v = *vertices; + float vOrigin = Atlas1D_RowId(texLoc) * Atlas1D.InvTileSize; + + float u1 = Drawer.MinBB.x; + float u2 = (count - 1) + Drawer.MaxBB.x * UV2_Scale; + float v1 = vOrigin + Drawer.MinBB.z * Atlas1D.InvTileSize; + float v2 = vOrigin + Drawer.MaxBB.z * Atlas1D.InvTileSize * UV2_Scale; + + float x1 = Drawer.X1, x2 = Drawer.X2 + (count - 1); + float y2 = Drawer.Y2; + float z1 = Drawer.Z1, z2 = Drawer.Z2; + + if (Drawer.Tinted) col = PackedCol_Tint(col, Drawer.TintCol); + + v->x = x2; v->y = y2; v->z = z1; v->Col = col; v->U = u2; v->V = v1; v++; + v->x = x1; v->y = y2; v->z = z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x1; v->y = y2; v->z = z2; v->Col = col; v->U = u1; v->V = v2; v++; + v->x = x2; v->y = y2; v->z = z2; v->Col = col; v->U = u2; v->V = v2; v++; + *vertices = v; +} diff --git a/src/Drawer.h b/src/Drawer.h new file mode 100644 index 0000000..cbc2a17 --- /dev/null +++ b/src/Drawer.h @@ -0,0 +1,38 @@ +#ifndef CC_DRAWER_H +#define CC_DRAWER_H +#include "PackedCol.h" +#include "Vectors.h" +/* +Draws the vertices for a cuboid region +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct VertexTextured; + +CC_VAR extern struct _DrawerData { + /* Whether a colour tinting effect should be applied to all faces. */ + cc_bool Tinted; + /* The colour to multiply colour of faces by (tinting effect). */ + PackedCol TintCol; + /* Minimum corner of base block bounding box. (For texture UV) */ + Vec3 MinBB; + /* Maximum corner of base block bounding box. (For texture UV) */ + Vec3 MaxBB; + /* Coordinate of minimum block bounding box corner in the world. */ + float X1, Y1, Z1; + /* Coordinate of maximum block bounding box corner in the world. */ + float X2, Y2, Z2; +} Drawer; + +/* Draws minimum X face of the cuboid. (i.e. at X1) */ +CC_API void Drawer_XMin(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices); +/* Draws maximum X face of the cuboid. (i.e. at X2) */ +CC_API void Drawer_XMax(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices); +/* Draws minimum Z face of the cuboid. (i.e. at Z1) */ +CC_API void Drawer_ZMin(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices); +/* Draws maximum Z face of the cuboid. (i.e. at Z2) */ +CC_API void Drawer_ZMax(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices); +/* Draws minimum Y face of the cuboid. (i.e. at Y1) */ +CC_API void Drawer_YMin(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices); +/* Draws maximum Y face of the cuboid. (i.e. at Y2) */ +CC_API void Drawer_YMax(int count, PackedCol col, TextureLoc texLoc, struct VertexTextured** vertices); +#endif diff --git a/src/Drawer2D.c b/src/Drawer2D.c new file mode 100644 index 0000000..7319024 --- /dev/null +++ b/src/Drawer2D.c @@ -0,0 +1,714 @@ +#include "Drawer2D.h" +#include "String.h" +#include "Graphics.h" +#include "Funcs.h" +#include "Platform.h" +#include "ExtMath.h" +#include "Logger.h" +#include "Game.h" +#include "Event.h" +#include "Chat.h" +#include "Stream.h" +#include "Utils.h" +#include "Errors.h" +#include "Window.h" +#include "Options.h" +#include "TexturePack.h" +#include "SystemFonts.h" + +struct _Drawer2DData Drawer2D; +#define Font_IsBitmap(font) (!(font)->handle) + +void DrawTextArgs_Make(struct DrawTextArgs* args, STRING_REF const cc_string* text, struct FontDesc* font, cc_bool useShadow) { + args->text = *text; + args->font = font; + args->useShadow = useShadow; +} + +void DrawTextArgs_MakeEmpty(struct DrawTextArgs* args, struct FontDesc* font, cc_bool useShadow) { + args->text = String_Empty; + args->font = font; + args->useShadow = useShadow; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Font functions------------------------------------------------------* +*#########################################################################################################################*/ +int Drawer2D_AdjHeight(int point) { return Math_CeilDiv(point * 3, 2); } + +void Font_MakeBitmapped(struct FontDesc* desc, int size, int flags) { + /* TODO: Scale X and Y independently */ + size = Display_ScaleY(size); + desc->handle = NULL; + desc->size = size; + desc->flags = flags; + desc->height = Drawer2D_AdjHeight(size); +} + +void Font_Make(struct FontDesc* desc, int size, int flags) { + if (Drawer2D.BitmappedText) { + Font_MakeBitmapped(desc, size, flags); + } else { + SysFont_MakeDefault(desc, size, flags); + } +} + +void Font_Free(struct FontDesc* desc) { + desc->size = 0; + if (Font_IsBitmap(desc)) return; + + SysFont_Free(desc); + desc->handle = NULL; +} + +static struct Bitmap fontBitmap; +static int tileSize = 8; /* avoid divide by 0 if default.png missing */ +/* So really 16 characters per row */ +#define LOG2_CHARS_PER_ROW 4 +static int tileWidths[256]; + +/* Finds the right-most non-transparent pixel in each tile in default.png */ +static void CalculateTextWidths(void) { + int width = fontBitmap.width, height = fontBitmap.height; + BitmapCol* row; + int i, x, y, xx, tileY; + + for (y = 0; y < height; y++) { + tileY = y / tileSize; + row = Bitmap_GetRow(&fontBitmap, y); + i = 0 | (tileY << LOG2_CHARS_PER_ROW); + + /* Iterate through each tile on current scanline */ + for (x = 0; x < width; x += tileSize, i++) { + /* Iterate through each pixel of the given character, on the current scanline */ + for (xx = tileSize - 1; xx >= 0; xx--) { + if (!BitmapCol_A(row[x + xx])) continue; + + /* Check if this is the pixel furthest to the right, for the current character */ + tileWidths[i] = max(tileWidths[i], xx + 1); + break; + } + } + } + tileWidths[' '] = tileSize / 4; +} + +static void FreeFontBitmap(void) { + int i; + for (i = 0; i < Array_Elems(tileWidths); i++) tileWidths[i] = 0; + Mem_Free(fontBitmap.scan0); +} + +cc_bool Font_SetBitmapAtlas(struct Bitmap* bmp) { + /* If not all of these cases are accounted for, end up overwriting memory after tileWidths */ + if (bmp->width != bmp->height) { + static const cc_string msg = String_FromConst("&cWidth of default.png must equal its height"); + Logger_WarnFunc(&msg); + return false; + } else if (bmp->width < 16) { + static const cc_string msg = String_FromConst("&cdefault.png must be at least 16 pixels wide"); + Logger_WarnFunc(&msg); + return false; + } else if (!Math_IsPowOf2(bmp->width)) { + static const cc_string msg = String_FromConst("&cWidth of default.png must be a power of two"); + Logger_WarnFunc(&msg); + return false; + } + + /* TODO: Use shift instead of mul/div */ + FreeFontBitmap(); + fontBitmap = *bmp; + tileSize = bmp->width >> LOG2_CHARS_PER_ROW; + + CalculateTextWidths(); + return true; +} + +void Font_SetPadding(struct FontDesc* desc, int amount) { + if (!Font_IsBitmap(desc)) return; + desc->height = desc->size + Display_ScaleY(amount) * 2; +} + + +/*########################################################################################################################* +*---------------------------------------------------Drawing functions-----------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Drawer2D_Clamp(struct Context2D* ctx, int* x, int* y, int* width, int* height) { + if (*x >= ctx->width || *y >= ctx->height) return false; + + /* origin is negative, move inside */ + if (*x < 0) { *width += *x; *x = 0; } + if (*y < 0) { *height += *y; *y = 0; } + + *width = min(*x + *width, ctx->width) - *x; + *height = min(*y + *height, ctx->height) - *y; + return *width > 0 && *height > 0; +} +#define Drawer2D_ClampPixel(p) p = (p < 0 ? 0 : (p > 255 ? 255 : p)) + +void Context2D_Alloc(struct Context2D* ctx, int width, int height) { + ctx->width = width; + ctx->height = height; + ctx->meta = NULL; + + if (!Gfx.SupportsNonPowTwoTextures) { + /* Allocate power-of-2 sized bitmap equal to or greater than the given size */ + width = Math_NextPowOf2(width); + height = Math_NextPowOf2(height); + } + + if (Gfx.MinTexWidth) { width = max(width, Gfx.MinTexWidth); } + if (Gfx.MinTexHeight) { height = max(height, Gfx.MinTexHeight); } + + ctx->bmp.width = width; + ctx->bmp.height = height; + ctx->bmp.scan0 = (BitmapCol*)Mem_AllocCleared(width * height, 4, "bitmap data"); +} + +void Context2D_Wrap(struct Context2D* ctx, struct Bitmap* bmp) { + ctx->bmp = *bmp; + ctx->width = bmp->width; + ctx->height = bmp->height; + ctx->meta = NULL; +} + +void Context2D_Free(struct Context2D* ctx) { + Mem_Free(ctx->bmp.scan0); +} + +#define BitmapColor_Raw(r, g, b) (BitmapColor_R_Bits(r) | BitmapColor_G_Bits(g) | BitmapColor_B_Bits(b)) +void Gradient_Noise(struct Context2D* ctx, BitmapCol color, int variation, + int x, int y, int width, int height) { + struct Bitmap* bmp = (struct Bitmap*)ctx; + BitmapCol* dst; + int R, G, B, xx, yy, n; + int noise, delta; + cc_uint32 alpha; + + if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; + alpha = color & BITMAPCOLOR_A_MASK; + + for (yy = 0; yy < height; yy++) { + dst = Bitmap_GetRow(bmp, y + yy) + x; + + for (xx = 0; xx < width; xx++, dst++) { + n = (x + xx) + (y + yy) * 57; + n = (n << 13) ^ n; + + /* + float noise = 1.0f - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0f; + int delta = (int)(noise * variation); + */ + /* Fixed point equivalent to the above expression */ + noise = ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff); + delta = (((1024 - noise / 0x100000)) * variation) >> 10; + + R = BitmapCol_R(color) + delta; Drawer2D_ClampPixel(R); + G = BitmapCol_G(color) + delta; Drawer2D_ClampPixel(G); + B = BitmapCol_B(color) + delta; Drawer2D_ClampPixel(B); + + *dst = BitmapColor_Raw(R, G, B) | alpha; + } + } +} + +void Gradient_Vertical(struct Context2D* ctx, BitmapCol a, BitmapCol b, + int x, int y, int width, int height) { + struct Bitmap* bmp = (struct Bitmap*)ctx; + BitmapCol* row, color; + int xx, yy; + float t; + if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; + + for (yy = 0; yy < height; yy++) { + row = Bitmap_GetRow(bmp, y + yy) + x; + t = (float)yy / (height - 1); /* so last row has color of b */ + + color = BitmapCol_Make( + Math_Lerp(BitmapCol_R(a), BitmapCol_R(b), t), + Math_Lerp(BitmapCol_G(a), BitmapCol_G(b), t), + Math_Lerp(BitmapCol_B(a), BitmapCol_B(b), t), + 255); + + for (xx = 0; xx < width; xx++) { row[xx] = color; } + } +} + +void Gradient_Blend(struct Context2D* ctx, BitmapCol color, int blend, + int x, int y, int width, int height) { + struct Bitmap* bmp = (struct Bitmap*)ctx; + BitmapCol* dst; + int R, G, B, xx, yy; + if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; + + /* Pre compute the alpha blended source color */ + /* TODO: Avoid shift when multiplying */ + color = BitmapCol_Make( + BitmapCol_R(color) * blend / 255, + BitmapCol_G(color) * blend / 255, + BitmapCol_B(color) * blend / 255, + 0); + blend = 255 - blend; /* inverse for existing pixels */ + + for (yy = 0; yy < height; yy++) { + dst = Bitmap_GetRow(bmp, y + yy) + x; + + for (xx = 0; xx < width; xx++, dst++) { + /* TODO: Not shift when multiplying */ + R = BitmapCol_R(color) + (BitmapCol_R(*dst) * blend) / 255; + G = BitmapCol_G(color) + (BitmapCol_G(*dst) * blend) / 255; + B = BitmapCol_B(color) + (BitmapCol_B(*dst) * blend) / 255; + + *dst = BitmapColor_RGB(R, G, B); + } + } +} + +void Context2D_DrawPixels(struct Context2D* ctx, int x, int y, struct Bitmap* src) { + struct Bitmap* dst = (struct Bitmap*)ctx; + int width = src->width, height = src->height; + BitmapCol* dstRow; + BitmapCol* srcRow; + int xx, yy; + if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; + + for (yy = 0; yy < height; yy++) { + srcRow = Bitmap_GetRow(src, yy); + dstRow = Bitmap_GetRow(dst, y + yy) + x; + + for (xx = 0; xx < width; xx++) { dstRow[xx] = srcRow[xx]; } + } +} + +void Context2D_Clear(struct Context2D* ctx, BitmapCol color, + int x, int y, int width, int height) { + struct Bitmap* bmp = (struct Bitmap*)ctx; + BitmapCol* row; + int xx, yy; + if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; + + for (yy = 0; yy < height; yy++) { + row = Bitmap_GetRow(bmp, y + yy) + x; + for (xx = 0; xx < width; xx++) { row[xx] = color; } + } +} + + +void Drawer2D_MakeTextTexture(struct Texture* tex, struct DrawTextArgs* args) { + static struct Texture empty = { 0, Tex_Rect(0,0, 0,0), Tex_UV(0,0, 1,1) }; + struct Context2D ctx; + int width, height; + /* pointless to draw anything when context is lost */ + if (Gfx.LostContext) { *tex = empty; return; } + + width = Drawer2D_TextWidth(args); + if (!width) { *tex = empty; return; } + height = Drawer2D_TextHeight(args); + + Context2D_Alloc(&ctx, width, height); + { + Context2D_DrawText(&ctx, args, 0, 0); + Context2D_MakeTexture(tex, &ctx); + } + Context2D_Free(&ctx); +} + +void Context2D_MakeTexture(struct Texture* tex, struct Context2D* ctx) { + int flags = TEXTURE_FLAG_NONPOW2 | TEXTURE_FLAG_LOWRES; + Gfx_RecreateTexture(&tex->ID, &ctx->bmp, flags, false); + + tex->width = ctx->width; + tex->height = ctx->height; + + tex->uv.u1 = 0.0f; tex->uv.v1 = 0.0f; + tex->uv.u2 = (float)ctx->width / (float)ctx->bmp.width; + tex->uv.v2 = (float)ctx->height / (float)ctx->bmp.height; +} + +cc_bool Drawer2D_ValidColorCodeAt(const cc_string* text, int i) { + if (i >= text->length) return false; + return BitmapCol_A(Drawer2D_GetColor(text->buffer[i])) != 0; +} + +cc_bool Drawer2D_UNSAFE_NextPart(cc_string* left, cc_string* part, char* colorCode) { + BitmapCol color; + char cur; + int i; + + /* check if current part starts with a colour code */ + if (left->length >= 2 && left->buffer[0] == '&') { + cur = left->buffer[1]; + color = Drawer2D_GetColor(cur); + + if (BitmapCol_A(color)) { + *colorCode = cur; + left->buffer += 2; + left->length -= 2; + } + } + + for (i = 0; i < left->length; i++) + { + if (left->buffer[i] == '&' && Drawer2D_ValidColorCodeAt(left, i + 1)) break; + } + + /* advance string starts and lengths */ + part->buffer = left->buffer; + part->length = i; + left->buffer += i; + left->length -= i; + + return part->length > 0 || left->length > 0; +} + +cc_bool Drawer2D_IsEmptyText(const cc_string* text) { + cc_string left = *text, part; + char colorCode; + + while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode)) + { + if (part.length) return false; + } + return true; +} + +void Drawer2D_WithoutColors(cc_string* str, const cc_string* src) { + cc_string left = *src, part; + char colorCode; + + while (Drawer2D_UNSAFE_NextPart(&left, &part, &colorCode)) + { + String_AppendString(str, &part); + } +} + +char Drawer2D_LastColor(const cc_string* text, int start) { + int i; + if (start >= text->length) start = text->length - 1; + + for (i = start; i >= 0; i--) { + if (text->buffer[i] != '&') continue; + if (Drawer2D_ValidColorCodeAt(text, i + 1)) { + return text->buffer[i + 1]; + } + } + return '\0'; +} +cc_bool Drawer2D_IsWhiteColor(char c) { return c == '\0' || c == 'f' || c == 'F'; } + +/* TODO: Needs to account for DPI */ +#define Drawer2D_ShadowOffset(point) (point / 8) +#define Drawer2D_XPadding(point) (Math_CeilDiv(point, 8)) +static int Drawer2D_Width(int point, char c) { + return Math_CeilDiv(tileWidths[(cc_uint8)c] * point, tileSize); +} + +void Drawer2D_ReducePadding_Tex(struct Texture* tex, int point, int scale) { + int padding; + float vAdj; + if (!Drawer2D.BitmappedText) return; + + padding = (tex->height - point) / scale; + vAdj = (float)padding / Math_NextPowOf2(tex->height); + tex->uv.v1 += vAdj; tex->uv.v2 -= vAdj; + tex->height -= (cc_uint16)(padding * 2); +} + +void Drawer2D_ReducePadding_Height(int* height, int point, int scale) { + int padding; + if (!Drawer2D.BitmappedText) return; + + padding = (*height - point) / scale; + *height -= padding * 2; +} + +void Drawer2D_Fill(struct Bitmap* bmp, int x, int y, int width, int height, BitmapCol color) { + BitmapCol* row; + int xx, yy; + + for (yy = y; yy < y + height; yy++) { + if (yy >= bmp->height) return; + row = Bitmap_GetRow(bmp, yy); + + for (xx = x; xx < x + width; xx++) { + if (xx >= bmp->width) break; + row[xx] = color; + } + } +} + +static void DrawBitmappedTextCore(struct Bitmap* bmp, struct DrawTextArgs* args, int x, int y, cc_bool shadow) { + BitmapCol color; + cc_string text = args->text; + int i, point = args->font->size, count = 0; + + int xPadding; + int srcX, srcY, dstX, dstY; + int fontX, fontY; + int srcWidth, dstWidth; + int dstHeight, begX, xx, yy; + int cellY, underlineY, underlineHeight; + + BitmapCol* srcRow, src; + BitmapCol* dstRow; + + cc_uint8 coords[DRAWER2D_MAX_TEXT_LENGTH]; + BitmapCol colors[DRAWER2D_MAX_TEXT_LENGTH]; + cc_uint16 dstWidths[DRAWER2D_MAX_TEXT_LENGTH]; + + color = Drawer2D.Colors['f']; + if (shadow) color = GetShadowColor(color); + + for (i = 0; i < text.length; i++) { + char c = text.buffer[i]; + if (c == '&' && Drawer2D_ValidColorCodeAt(&text, i + 1)) { + color = Drawer2D_GetColor(text.buffer[i + 1]); + + if (shadow) color = GetShadowColor(color); + i++; continue; /* skip over the color code */ + } + + coords[count] = c; + colors[count] = color; + dstWidths[count] = Drawer2D_Width(point, c); + count++; + } + + dstHeight = point; begX = x; + /* adjust coords to make drawn text match GDI fonts */ + y += (args->font->height - dstHeight) / 2; + xPadding = Drawer2D_XPadding(point); + + for (yy = 0; yy < dstHeight; yy++) { + dstY = y + yy; + if ((unsigned)dstY >= (unsigned)bmp->height) continue; + + fontY = 0 + yy * tileSize / dstHeight; + dstRow = Bitmap_GetRow(bmp, dstY); + + for (i = 0; i < count; i++) { + srcX = (coords[i] & 0x0F) * tileSize; + srcY = (coords[i] >> 4) * tileSize; + srcRow = Bitmap_GetRow(&fontBitmap, fontY + srcY); + + srcWidth = tileWidths[coords[i]]; + dstWidth = dstWidths[i]; + color = colors[i]; + + for (xx = 0; xx < dstWidth; xx++) { + fontX = srcX + xx * srcWidth / dstWidth; + src = srcRow[fontX]; + if (!BitmapCol_A(src)) continue; + + dstX = x + xx; + if ((unsigned)dstX >= (unsigned)bmp->width) continue; + + /* TODO: Transparent text by multiplying by col.A */ + /* TODO: Not shift when multiplying */ + /* TODO: avoid BitmapCol_A shift */ + dstRow[dstX] = BitmapCol_Make( + BitmapCol_R(src) * BitmapCol_R(color) / 255, + BitmapCol_G(src) * BitmapCol_G(color) / 255, + BitmapCol_B(src) * BitmapCol_B(color) / 255, + BitmapCol_A(src)); + } + x += dstWidth + xPadding; + } + x = begX; + } + + if (!(args->font->flags & FONT_FLAGS_UNDERLINE)) return; + /* scale up bottom row of a cell to drawn text font */ + cellY = (8 - 1) * dstHeight / 8; + underlineY = y + cellY; + underlineHeight = dstHeight - cellY; + + for (i = 0; i < count; ) { + dstWidth = 0; + color = colors[i]; + + for (; i < count && color == colors[i]; i++) { + dstWidth += dstWidths[i] + xPadding; + } + Drawer2D_Fill(bmp, x, underlineY, dstWidth, underlineHeight, color); + x += dstWidth; + } +} + +static void DrawBitmappedText(struct Bitmap* bmp, struct DrawTextArgs* args, int x, int y) { + int offset = Drawer2D_ShadowOffset(args->font->size); + + if (!fontBitmap.scan0) { + if (args->useShadow) FallbackFont_DrawText(args, bmp, x, y, true); + FallbackFont_DrawText(args, bmp, x, y, false); + return; + } + + if (args->useShadow) { + DrawBitmappedTextCore(bmp, args, x + offset, y + offset, true); + } + DrawBitmappedTextCore(bmp, args, x, y, false); +} + +static int MeasureBitmappedWidth(const struct DrawTextArgs* args) { + int i, point = args->font->size; + int xPadding, width; + cc_string text; + + if (!fontBitmap.scan0) return FallbackFont_TextWidth(args); + + /* adjust coords to make drawn text match GDI fonts */ + xPadding = Drawer2D_XPadding(point); + width = 0; + + text = args->text; + for (i = 0; i < text.length; i++) { + char c = text.buffer[i]; + if (c == '&' && Drawer2D_ValidColorCodeAt(&text, i + 1)) { + i++; continue; /* skip over the color code */ + } + width += Drawer2D_Width(point, c) + xPadding; + } + if (!width) return 0; + + /* Remove padding at end */ + if (!(args->font->flags & FONT_FLAGS_PADDING)) width -= xPadding; + + if (args->useShadow) { width += Drawer2D_ShadowOffset(point); } + return width; +} + +void Context2D_DrawText(struct Context2D* ctx, struct DrawTextArgs* args, int x, int y) { + struct Bitmap* bmp = (struct Bitmap*)ctx; + if (Drawer2D_IsEmptyText(&args->text)) return; + if (Font_IsBitmap(args->font)) { DrawBitmappedText(bmp, args, x, y); return; } + + if (args->useShadow) { SysFont_DrawText(args, bmp, x, y, true); } + SysFont_DrawText(args, bmp, x, y, false); +} + +int Drawer2D_TextWidth(struct DrawTextArgs* args) { + if (Font_IsBitmap(args->font)) return MeasureBitmappedWidth(args); + return SysFont_TextWidth(args); +} + +int Drawer2D_TextHeight(struct DrawTextArgs* args) { + return Font_CalcHeight(args->font, args->useShadow); +} + +int Font_CalcHeight(const struct FontDesc* font, cc_bool useShadow) { + int height = font->height; + if (Font_IsBitmap(font)) { + if (useShadow) { height += Drawer2D_ShadowOffset(font->size); } + } else { + if (useShadow) height += 2; + } + return height; +} + +void Drawer2D_DrawClippedText(struct Context2D* ctx, struct DrawTextArgs* args, + int x, int y, int maxWidth) { + char strBuffer[512]; + struct DrawTextArgs part; + int i, width; + + width = Drawer2D_TextWidth(args); + /* No clipping needed */ + if (width <= maxWidth) { Context2D_DrawText(ctx, args, x, y); return; } + part = *args; + + String_InitArray(part.text, strBuffer); + String_Copy(&part.text, &args->text); + String_Append(&part.text, '.'); + + for (i = part.text.length - 2; i > 0; i--) { + part.text.buffer[i] = '.'; + /* skip over trailing spaces */ + if (part.text.buffer[i - 1] == ' ') continue; + + part.text.length = i + 2; + width = Drawer2D_TextWidth(&part); + if (width <= maxWidth) { Context2D_DrawText(ctx, &part, x, y); return; } + + /* If down to <= 2 chars, try omitting the .. */ + if (i > 2) continue; + part.text.length = i; + width = Drawer2D_TextWidth(&part); + if (width <= maxWidth) { Context2D_DrawText(ctx, &part, x, y); return; } + } +} + + +/*########################################################################################################################* +*---------------------------------------------------Drawer2D component----------------------------------------------------* +*#########################################################################################################################*/ +static void DefaultPngProcess(struct Stream* stream, const cc_string* name) { + struct Bitmap bmp; + cc_result res; + + if ((res = Png_Decode(&bmp, stream))) { + Logger_SysWarn2(res, "decoding", name); + Mem_Free(bmp.scan0); + } else if (Font_SetBitmapAtlas(&bmp)) { + Event_RaiseVoid(&ChatEvents.FontChanged); + } else { + Mem_Free(bmp.scan0); + } +} +static struct TextureEntry default_entry = { "default.png", DefaultPngProcess }; + + +/* The default 16 colours are the CGA 16 color palette (without special brown colour) */ +/* See https://en.wikipedia.org/wiki/Color_Graphics_Adapter#With_an_RGBI_monitor for reference */ +/* The 16 hex colours below were produced from the following formula: */ +/* R = 191 * ((hex >> 2) & 1) + 64 * (hex >> 3) */ +/* G = 191 * ((hex >> 1) & 1) + 64 * (hex >> 3) */ +/* B = 191 * ((hex >> 0) & 1) + 64 * (hex >> 3) */ +static const BitmapCol defaults_0_9[] = { + BitmapColor_RGB( 0, 0, 0), /* 0 */ + BitmapColor_RGB( 0, 0, 191), /* 1 */ + BitmapColor_RGB( 0, 191, 0), /* 2 */ + BitmapColor_RGB( 0, 191, 191), /* 3 */ + BitmapColor_RGB(191, 0, 0), /* 4 */ + BitmapColor_RGB(191, 0, 191), /* 5 */ + BitmapColor_RGB(191, 191, 0), /* 6 */ + BitmapColor_RGB(191, 191, 191), /* 7 */ + BitmapColor_RGB( 64, 64, 64), /* 8 */ + BitmapColor_RGB( 64, 64, 255) /* 9 */ +}; +static const BitmapCol defaults_a_f[] = { + BitmapColor_RGB( 64, 255, 64), /* A */ + BitmapColor_RGB( 64, 255, 255), /* B */ + BitmapColor_RGB(255, 64, 64), /* C */ + BitmapColor_RGB(255, 64, 255), /* D */ + BitmapColor_RGB(255, 255, 64), /* E */ + BitmapColor_RGB(255, 255, 255), /* F */ +}; + +static void OnReset(void) { + Mem_Set(Drawer2D.Colors, 0, sizeof(Drawer2D.Colors)); + + Mem_Copy(&Drawer2D.Colors['0'], defaults_0_9, sizeof(defaults_0_9)); + Mem_Copy(&Drawer2D.Colors['a'], defaults_a_f, sizeof(defaults_a_f)); + Mem_Copy(&Drawer2D.Colors['A'], defaults_a_f, sizeof(defaults_a_f)); +} + +static void OnInit(void) { + OnReset(); + TextureEntry_Register(&default_entry); + + Drawer2D.BitmappedText = Game_ClassicMode || !Options_GetBool(OPT_USE_CHAT_FONT, false); + Drawer2D.BlackTextShadows = Options_GetBool(OPT_BLACK_TEXT, false); +} + +static void OnFree(void) { + FreeFontBitmap(); + fontBitmap.scan0 = NULL; +} + +struct IGameComponent Drawer2D_Component = { + OnInit, /* Init */ + OnFree, /* Free */ + OnReset, /* Reset */ +}; diff --git a/src/Drawer2D.h b/src/Drawer2D.h new file mode 100644 index 0000000..1d957e4 --- /dev/null +++ b/src/Drawer2D.h @@ -0,0 +1,129 @@ +#ifndef CC_DRAWER2D_H +#define CC_DRAWER2D_H +#include "Bitmap.h" +#include "Constants.h" +/* Performs a variety of drawing operations on bitmaps, and converts bitmaps into textures. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +enum FONT_FLAGS { FONT_FLAGS_NONE = 0x00, FONT_FLAGS_BOLD = 0x01, FONT_FLAGS_UNDERLINE = 0x02, FONT_FLAGS_PADDING = 0x04 }; +struct FontDesc { void* handle; cc_uint16 size, flags; int height; }; +struct DrawTextArgs { cc_string text; struct FontDesc* font; cc_bool useShadow; }; +struct Context2D { struct Bitmap bmp; int width, height; void* meta; }; +struct Texture; +struct IGameComponent; +extern struct IGameComponent Drawer2D_Component; + +#define DRAWER2D_MAX_TEXT_LENGTH 256 + +CC_VAR extern struct _Drawer2DData { + /* Whether text should be drawn and measured using the currently set font bitmap */ + /* If false, then text is instead draw using platform/system fonts */ + cc_bool BitmappedText; + /* Whether the shadows behind text (that uses shadows) is fully black */ + cc_bool BlackTextShadows; + /* List of all colors (An A of 0 means the color is not used) */ + BitmapCol Colors[DRAWER2D_MAX_COLORS]; +} Drawer2D; + +#define Drawer2D_GetColor(c) Drawer2D.Colors[(cc_uint8)c] +void DrawTextArgs_Make(struct DrawTextArgs* args, STRING_REF const cc_string* text, struct FontDesc* font, cc_bool useShadow); +void DrawTextArgs_MakeEmpty(struct DrawTextArgs* args, struct FontDesc* font, cc_bool useShadow); + +/* Clamps the given rectangle to lie inside the bitmap */ +/* Returns false if rectangle is completely outside bitmap's rectangle */ +cc_bool Drawer2D_Clamp(struct Context2D* ctx, int* x, int* y, int* width, int* height); + +/* Allocates a new context for 2D drawing */ +/* Note: Allocates a power-of-2 sized backing bitmap equal to or greater than the given size */ +CC_API void Context2D_Alloc(struct Context2D* ctx, int width, int height); +/* Allocates a new context for 2D drawing, using an existing bimap as backing bitmap */ +CC_API void Context2D_Wrap(struct Context2D* ctx, struct Bitmap* bmp); +/* Frees/Releases a previously allocatedcontext for 2D drawing */ +CC_API void Context2D_Free(struct Context2D* ctx); +/* Creates a texture consisting of the pixels from the backing bitmap of the given 2D context */ +CC_API void Context2D_MakeTexture(struct Texture* tex, struct Context2D* ctx); + +/* Draws text using the given font at the given coordinates */ +CC_API void Context2D_DrawText(struct Context2D* ctx, struct DrawTextArgs* args, int x, int y); +/* Fills the given area using pixels from the source bitmap */ +CC_API void Context2D_DrawPixels(struct Context2D* ctx, int x, int y, struct Bitmap* src); +/* Fills the area with the given color */ +CC_API void Context2D_Clear(struct Context2D* ctx, BitmapCol color, + int x, int y, int width, int height); + +/* Fills the given area with a simple noisy pattern */ +/* Variation determines how 'visible/obvious' the noise is */ +CC_API void Gradient_Noise(struct Context2D* ctx, BitmapCol color, int variation, + int x, int y, int width, int height); +/* Fills the given area with a vertical gradient, linerarly interpolating from a to b */ +/* First drawn row is filled with 'a', while last drawn is filled with 'b' */ +CC_API void Gradient_Vertical(struct Context2D* ctx, BitmapCol a, BitmapCol b, + int x, int y, int width, int height); +/* Blends the given area with the given color */ +/* Note that this only blends RGB, A is not blended */ +CC_API void Gradient_Blend(struct Context2D* ctx, BitmapCol color, int blend, + int x, int y, int width, int height); + +/* Returns how wide the given text would be when drawn */ +CC_API int Drawer2D_TextWidth(struct DrawTextArgs* args); +/* Returns how tall the given text would be when drawn */ +/* NOTE: Height returned only depends on the font. (see Font_CalcHeight) */ +CC_API int Drawer2D_TextHeight(struct DrawTextArgs* args); +/* Similar to Context2D_DrawText, but trims the text with trailing ".." if wider than maxWidth */ +void Drawer2D_DrawClippedText(struct Context2D* ctx, struct DrawTextArgs* args, + int x, int y, int maxWidth); + +/* Creates a texture consisting only of the given text drawn onto it */ +/* NOTE: The returned texture is always padded up to nearest power of two dimensions */ +CC_API void Drawer2D_MakeTextTexture(struct Texture* tex, struct DrawTextArgs* args); + +/* Returns whether the given color code is used/valid */ +/* NOTE: This can change if the server defines custom color codes */ +cc_bool Drawer2D_ValidColorCodeAt(const cc_string* text, int i); +/* Whether text is empty or consists purely of valid color codes */ +cc_bool Drawer2D_IsEmptyText(const cc_string* text); +/* Copies all characters from str into src, except for used color codes */ +/* NOTE: Ampersands not followed by a used color code are still copied */ +void Drawer2D_WithoutColors(cc_string* str, const cc_string* src); +/* Returns the last valid color code in the given input, or \0 if not found */ +char Drawer2D_LastColor(const cc_string* text, int start); +/* Returns whether the color code is f, F or \0 */ +cc_bool Drawer2D_IsWhiteColor(char c); +cc_bool Drawer2D_UNSAFE_NextPart(cc_string* left, cc_string* part, char* colorCode); + +/* Divides R/G/B by 4 */ +#define SHADOW_MASK ((0x3F << BITMAPCOLOR_R_SHIFT) | (0x3F << BITMAPCOLOR_G_SHIFT) | (0x3F << BITMAPCOLOR_B_SHIFT)) +static CC_INLINE BitmapCol GetShadowColor(BitmapCol c) { + if (Drawer2D.BlackTextShadows) return BITMAPCOLOR_BLACK; + + /* Initial layout: aaaa_aaaa|rrrr_rrrr|gggg_gggg|bbbb_bbbb */ + /* Shift right 2: 00aa_aaaa|aarr_rrrr|rrgg_gggg|ggbb_bbbb */ + /* And by 3f3f3f: 0000_0000|00rr_rrrr|00gg_gggg|00bb_bbbb */ + /* Or by alpha : aaaa_aaaa|00rr_rrrr|00gg_gggg|00bb_bbbb */ + return (c & BITMAPCOLOR_A_MASK) | ((c >> 2) & SHADOW_MASK); +} + +/* Allocates a new instance of the default font using the given size and flags */ +/* Uses Font_MakeBitmapped or SysFont_MakeDefault depending on Drawer2D_BitmappedText */ +CC_API void Font_Make(struct FontDesc* desc, int size, int flags); +/* Frees an allocated font */ +CC_API void Font_Free(struct FontDesc* desc); +/* Returns the line height for drawing text using the given font */ +int Font_CalcHeight(const struct FontDesc* font, cc_bool useShadow); +/* Adjusts height to be closer to system fonts */ +int Drawer2D_AdjHeight(int point); + +void Drawer2D_ReducePadding_Tex(struct Texture* tex, int point, int scale); +void Drawer2D_ReducePadding_Height(int* height, int point, int scale); +/* Quickly fills the given box region */ +void Drawer2D_Fill(struct Bitmap* bmp, int x, int y, int width, int height, BitmapCol color); + +/* Sets the bitmap used for drawing bitmapped fonts. (i.e. default.png) */ +/* The bitmap must be square and consist of a 16x16 tile layout */ +cc_bool Font_SetBitmapAtlas(struct Bitmap* bmp); +/* Sets padding for a bitmapped font */ +void Font_SetPadding(struct FontDesc* desc, int amount); +/* Initialises the given font for drawing bitmapped text using default.png */ +void Font_MakeBitmapped(struct FontDesc* desc, int size, int flags); +#endif diff --git a/src/Entity.c b/src/Entity.c new file mode 100644 index 0000000..b400c85 --- /dev/null +++ b/src/Entity.c @@ -0,0 +1,1107 @@ +#include "Entity.h" +#include "ExtMath.h" +#include "World.h" +#include "Block.h" +#include "Event.h" +#include "Game.h" +#include "Camera.h" +#include "Platform.h" +#include "Funcs.h" +#include "Graphics.h" +#include "Lighting.h" +#include "Http.h" +#include "Chat.h" +#include "Model.h" +#include "Input.h" +#include "Gui.h" +#include "Stream.h" +#include "Bitmap.h" +#include "Logger.h" +#include "Options.h" +#include "Errors.h" +#include "Utils.h" +#include "EntityRenderers.h" + +const char* const NameMode_Names[NAME_MODE_COUNT] = { "None", "Hovered", "All", "AllHovered", "AllUnscaled" }; +const char* const ShadowMode_Names[SHADOW_MODE_COUNT] = { "None", "SnapToBlock", "Circle", "CircleAll" }; + + +/*########################################################################################################################* +*---------------------------------------------------------Entity----------------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol Entity_GetColor(struct Entity* e) { + Vec3 eyePos = Entity_GetEyePosition(e); + IVec3 pos; IVec3_Floor(&pos, &eyePos); + return Lighting.Color(pos.x, pos.y, pos.z); +} + +void Entity_Init(struct Entity* e) { + static const cc_string model = String_FromConst("humanoid"); + Vec3_Set(e->ModelScale, 1,1,1); + e->Flags = ENTITY_FLAG_HAS_MODELVB; + e->uScale = 1.0f; + e->vScale = 1.0f; + e->_skinReqID = 0; + e->SkinRaw[0] = '\0'; + e->NameRaw[0] = '\0'; + Entity_SetModel(e, &model); +} + +void Entity_SetName(struct Entity* e, const cc_string* name) { + EntityNames_Delete(e); + String_CopyToRawArray(e->NameRaw, name); +} + +Vec3 Entity_GetEyePosition(struct Entity* e) { + Vec3 pos = e->Position; pos.y += Entity_GetEyeHeight(e); return pos; +} + +float Entity_GetEyeHeight(struct Entity* e) { + return e->Model->GetEyeY(e) * e->ModelScale.y; +} + +void Entity_GetTransform(struct Entity* e, Vec3 pos, Vec3 scale, struct Matrix* m) { + struct Matrix tmp; + Matrix_Scale(m, scale.x, scale.y, scale.z); + + if (e->RotZ) { + Matrix_RotateZ( &tmp, -e->RotZ * MATH_DEG2RAD); + Matrix_MulBy(m, &tmp); + } + if (e->RotX) { + Matrix_RotateX( &tmp, -e->RotX * MATH_DEG2RAD); + Matrix_MulBy(m, &tmp); + } + if (e->RotY) { + Matrix_RotateY( &tmp, -e->RotY * MATH_DEG2RAD); + Matrix_MulBy(m, &tmp); + } + + Matrix_Translate(&tmp, pos.x, pos.y, pos.z); + Matrix_MulBy(m, &tmp); + /* return scale * rotZ * rotX * rotY * translate; */ +} + +void Entity_GetPickingBounds(struct Entity* e, struct AABB* bb) { + AABB_Offset(bb, &e->ModelAABB, &e->Position); +} + +void Entity_GetBounds(struct Entity* e, struct AABB* bb) { + AABB_Make(bb, &e->Position, &e->Size); +} + +static void Entity_ParseScale(struct Entity* e, const cc_string* scale) { + float value; + if (!Convert_ParseFloat(scale, &value)) return; + value = max(value, 0.001f); + + /* local player doesn't allow giant model scales */ + /* (can't climb stairs, extremely CPU intensive collisions) */ + if (e->Flags & ENTITY_FLAG_MODEL_RESTRICTED_SCALE) { + value = min(value, e->Model->maxScale); + } + Vec3_Set(e->ModelScale, value,value,value); +} + +static void Entity_SetBlockModel(struct Entity* e, const cc_string* model) { + static const cc_string block = String_FromConst("block"); + int raw = Block_Parse(model); + + if (raw == -1) { + /* use default humanoid model */ + e->Model = Models.Human; + } else { + e->ModelBlock = (BlockID)raw; + e->Model = Model_Get(&block); + } +} + +void Entity_SetModel(struct Entity* e, const cc_string* model) { + cc_string name, scale; + Vec3_Set(e->ModelScale, 1,1,1); + String_UNSAFE_Separate(model, '|', &name, &scale); + + /* 'giant' model kept for backwards compatibility */ + if (String_CaselessEqualsConst(&name, "giant")) { + name = String_FromReadonly("humanoid"); + Vec3_Set(e->ModelScale, 2,2,2); + } + + e->ModelBlock = BLOCK_AIR; + e->Model = Model_Get(&name); + if (!e->Model) Entity_SetBlockModel(e, &name); + + Entity_ParseScale(e, &scale); + Entity_UpdateModelBounds(e); + + if (e->Flags & ENTITY_FLAG_HAS_MODELVB) + Gfx_DeleteDynamicVb(&e->ModelVB); +} + +void Entity_UpdateModelBounds(struct Entity* e) { + struct Model* model = e->Model; + model->GetCollisionSize(e); + model->GetPickingBounds(e); + + Vec3_Mul3By(&e->Size, &e->ModelScale); + Vec3_Mul3By(&e->ModelAABB.Min, &e->ModelScale); + Vec3_Mul3By(&e->ModelAABB.Max, &e->ModelScale); +} + +cc_bool Entity_TouchesAny(struct AABB* bounds, Entity_TouchesCondition condition) { + IVec3 bbMin, bbMax; + BlockID block; + struct AABB blockBB; + Vec3 v; + int x, y, z; + + IVec3_Floor(&bbMin, &bounds->Min); + IVec3_Floor(&bbMax, &bounds->Max); + + bbMin.x = max(bbMin.x, 0); bbMax.x = min(bbMax.x, World.MaxX); + bbMin.y = max(bbMin.y, 0); bbMax.y = min(bbMax.y, World.MaxY); + bbMin.z = max(bbMin.z, 0); bbMax.z = min(bbMax.z, World.MaxZ); + + for (y = bbMin.y; y <= bbMax.y; y++) { v.y = (float)y; + for (z = bbMin.z; z <= bbMax.z; z++) { v.z = (float)z; + for (x = bbMin.x; x <= bbMax.x; x++) { v.x = (float)x; + + block = World_GetBlock(x, y, z); + Vec3_Add(&blockBB.Min, &v, &Blocks.MinBB[block]); + Vec3_Add(&blockBB.Max, &v, &Blocks.MaxBB[block]); + + if (!AABB_Intersects(&blockBB, bounds)) continue; + if (condition(block)) return true; + } + } + } + return false; +} + +static cc_bool IsRopeCollide(BlockID b) { return Blocks.ExtendedCollide[b] == COLLIDE_CLIMB; } +cc_bool Entity_TouchesAnyRope(struct Entity* e) { + struct AABB bounds; Entity_GetBounds(e, &bounds); + bounds.Max.y += 0.5f / 16.0f; + return Entity_TouchesAny(&bounds, IsRopeCollide); +} + +static const Vec3 entity_liqExpand = { 0.25f/16.0f, 0.0f/16.0f, 0.25f/16.0f }; +static cc_bool IsLavaCollide(BlockID b) { return Blocks.ExtendedCollide[b] == COLLIDE_LAVA; } +cc_bool Entity_TouchesAnyLava(struct Entity* e) { + struct AABB bounds; Entity_GetBounds(e, &bounds); + AABB_Offset(&bounds, &bounds, &entity_liqExpand); + return Entity_TouchesAny(&bounds, IsLavaCollide); +} + +static cc_bool IsWaterCollide(BlockID b) { return Blocks.ExtendedCollide[b] == COLLIDE_WATER; } +cc_bool Entity_TouchesAnyWater(struct Entity* e) { + struct AABB bounds; Entity_GetBounds(e, &bounds); + AABB_Offset(&bounds, &bounds, &entity_liqExpand); + return Entity_TouchesAny(&bounds, IsWaterCollide); +} + + +/*########################################################################################################################* +*------------------------------------------------------Entity skins-------------------------------------------------------* +*#########################################################################################################################*/ +static struct Entity* Entity_FirstOtherWithSameSkinAndFetchedSkin(struct Entity* except) { + struct Entity* e; + cc_string skin, eSkin; + int i; + + skin = String_FromRawArray(except->SkinRaw); + for (i = 0; i < ENTITIES_MAX_COUNT; i++) { + if (!Entities.List[i] || Entities.List[i] == except) continue; + + e = Entities.List[i]; + eSkin = String_FromRawArray(e->SkinRaw); + if (e->SkinFetchState && String_Equals(&skin, &eSkin)) return e; + } + return NULL; +} + +/* Copies skin data from another entity */ +static void Entity_CopySkin(struct Entity* dst, struct Entity* src) { + dst->TextureId = src->TextureId; + dst->SkinType = src->SkinType; + dst->uScale = src->uScale; + dst->vScale = src->vScale; + dst->MobTextureId = src->MobTextureId; +} + +/* Resets skin data for the given entity */ +static void Entity_ResetSkin(struct Entity* e) { + e->uScale = 1.0f; e->vScale = 1.0f; + e->MobTextureId = 0; + e->TextureId = 0; + e->SkinType = SKIN_64x32; +} + +/* Copies or resets skin data for all entity with same skin */ +static void Entity_SetSkinAll(struct Entity* source, cc_bool reset) { + struct Entity* e; + cc_string skin, eSkin; + int i; + + skin = String_FromRawArray(source->SkinRaw); + source->MobTextureId = Utils_IsUrlPrefix(&skin) ? source->TextureId : 0; + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) { + if (!Entities.List[i]) continue; + + e = Entities.List[i]; + eSkin = String_FromRawArray(e->SkinRaw); + if (!String_Equals(&skin, &eSkin)) continue; + + if (reset) { + Entity_ResetSkin(e); + } else { + Entity_CopySkin(e, source); + } + e->SkinFetchState = SKIN_FETCH_COMPLETED; + } +} + +/* Clears hat area from a skin bitmap if it's completely white or black, + so skins edited with Microsoft Paint or similiar don't have a solid hat */ +static void Entity_ClearHat(struct Bitmap* bmp, cc_uint8 skinType) { + int sizeX = (bmp->width / 64) * 32; + int yScale = skinType == SKIN_64x32 ? 32 : 64; + int sizeY = (bmp->height / yScale) * 16; + int x, y; + + /* determine if we actually need filtering */ + for (y = 0; y < sizeY; y++) { + BitmapCol* row = Bitmap_GetRow(bmp, y) + sizeX; + for (x = 0; x < sizeX; x++) { + if (BitmapCol_A(row[x]) != 255) return; + } + } + + /* only perform filtering when the entire hat is opaque */ + for (y = 0; y < sizeY; y++) { + BitmapCol* row = Bitmap_GetRow(bmp, y) + sizeX; + for (x = 0; x < sizeX; x++) { + BitmapCol c = row[x]; + if (c == BITMAPCOLOR_WHITE || c == BITMAPCOLOR_BLACK) row[x] = 0; + } + } +} + +/* Ensures skin is a power of two size, resizing if needed. */ +static cc_result EnsurePow2Skin(struct Entity* e, struct Bitmap* bmp) { + struct Bitmap scaled; + cc_uint32 stride; + int width, height; + int y; + + width = Math_NextPowOf2(bmp->width); + height = Math_NextPowOf2(bmp->height); + if (width == bmp->width && height == bmp->height) return 0; + + Bitmap_TryAllocate(&scaled, width, height); + if (!scaled.scan0) return ERR_OUT_OF_MEMORY; + + e->uScale = (float)bmp->width / width; + e->vScale = (float)bmp->height / height; + stride = bmp->width * 4; + + for (y = 0; y < bmp->height; y++) { + BitmapCol* src = Bitmap_GetRow(bmp, y); + BitmapCol* dst = Bitmap_GetRow(&scaled, y); + Mem_Copy(dst, src, stride); + } + + Mem_Free(bmp->scan0); + *bmp = scaled; + return 0; +} + +static cc_result ApplySkin(struct Entity* e, struct Bitmap* bmp, struct Stream* src, cc_string* skin) { + cc_result res; + if ((res = Png_Decode(bmp, src))) return res; + + Gfx_DeleteTexture(&e->TextureId); + Entity_SetSkinAll(e, true); + if ((res = EnsurePow2Skin(e, bmp))) return res; + e->SkinType = Utils_CalcSkinType(bmp); + + if (!Gfx_CheckTextureSize(bmp->width, bmp->height, 0)) { + Chat_Add1("&cSkin %s is too large", skin); + } else { + if (e->Model->flags & MODEL_FLAG_CLEAR_HAT) + Entity_ClearHat(bmp, e->SkinType); + + e->TextureId = Gfx_CreateTexture(bmp, TEXTURE_FLAG_MANAGED, false); + Entity_SetSkinAll(e, false); + } + return 0; +} + +static void LogInvalidSkin(cc_result res, const cc_string* skin, const cc_uint8* data, int size) { + cc_string msg; char msgBuffer[256]; + String_InitArray(msg, msgBuffer); + + Logger_FormatWarn2(&msg, res, "decoding skin", skin, Platform_DescribeError); + if (res != PNG_ERR_INVALID_SIG) { Logger_WarnFunc(&msg); return; } + + String_AppendConst(&msg, " (got "); + String_AppendAll( &msg, data, min(size, 8)); + String_AppendConst(&msg, ")"); + Logger_WarnFunc(&msg); +} + +static void Entity_CheckSkin(struct Entity* e) { + struct Entity* first; + struct HttpRequest item; + struct Stream mem; + struct Bitmap bmp; + cc_string skin; + cc_uint8 flags; + cc_result res; + + /* Don't check skin if don't have to */ + if (!e->Model->usesSkin) return; + if (e->SkinFetchState == SKIN_FETCH_COMPLETED) return; + skin = String_FromRawArray(e->SkinRaw); + + if (!e->SkinFetchState) { + first = Entity_FirstOtherWithSameSkinAndFetchedSkin(e); + flags = e == &LocalPlayer_Instances[0].Base ? HTTP_FLAG_NOCACHE : 0; + + if (!first) { + e->_skinReqID = Http_AsyncGetSkin(&skin, flags); + e->SkinFetchState = SKIN_FETCH_DOWNLOADING; + } else { + Entity_CopySkin(e, first); + e->SkinFetchState = SKIN_FETCH_COMPLETED; + return; + } + } + + if (!Http_GetResult(e->_skinReqID, &item)) return; + + if (!item.success) { + Entity_SetSkinAll(e, true); + } else { + Stream_ReadonlyMemory(&mem, item.data, item.size); + + if ((res = ApplySkin(e, &bmp, &mem, &skin))) { + LogInvalidSkin(res, &skin, item.data, item.size); + } + Mem_Free(bmp.scan0); + } + HttpRequest_Free(&item); +} + +/* Returns true if no other entities are sharing this skin texture */ +static cc_bool CanDeleteTexture(struct Entity* except) { + int i; + if (!except->TextureId) return false; + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) { + if (!Entities.List[i] || Entities.List[i] == except) continue; + if (Entities.List[i]->TextureId == except->TextureId) return false; + } + return true; +} + +CC_NOINLINE static void DeleteSkin(struct Entity* e) { + if (CanDeleteTexture(e)) Gfx_DeleteTexture(&e->TextureId); + + Entity_ResetSkin(e); + e->SkinFetchState = 0; +} + +void Entity_SetSkin(struct Entity* e, const cc_string* skin) { + cc_string tmp; char tmpBuffer[STRING_SIZE]; + DeleteSkin(e); + + if (Utils_IsUrlPrefix(skin)) { + tmp = *skin; + } else { + String_InitArray(tmp, tmpBuffer); + String_AppendColorless(&tmp, skin); + } + String_CopyToRawArray(e->SkinRaw, &tmp); +} + +void Entity_LerpAngles(struct Entity* e, float t) { + struct EntityLocation* prev = &e->prev; + struct EntityLocation* next = &e->next; + + e->Pitch = Math_LerpAngle(prev->pitch, next->pitch, t); + e->Yaw = Math_LerpAngle(prev->yaw, next->yaw, t); + e->RotX = Math_LerpAngle(prev->rotX, next->rotX, t); + e->RotY = Math_LerpAngle(prev->rotY, next->rotY, t); + e->RotZ = Math_LerpAngle(prev->rotZ, next->rotZ, t); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Entities---------------------------------------------------------* +*#########################################################################################################################*/ +struct _EntitiesData Entities; + +void Entities_Tick(struct ScheduledTask* task) { + int i; + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + if (!Entities.List[i]) continue; + Entities.List[i]->VTABLE->Tick(Entities.List[i], task->interval); + } +} + +void Entities_RenderModels(float delta, float t) { + int i; + Gfx_SetAlphaTest(true); + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + if (!Entities.List[i]) continue; + Entities.List[i]->VTABLE->RenderModel(Entities.List[i], delta, t); + } + Gfx_SetAlphaTest(false); +} + +static void Entities_ContextLost(void* obj) { + struct Entity* entity; + int i; + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + entity = Entities.List[i]; + if (!entity) continue; + + if (entity->Flags & ENTITY_FLAG_HAS_MODELVB) + Gfx_DeleteDynamicVb(&entity->ModelVB); + + if (!Gfx.ManagedTextures) + DeleteSkin(entity); + } +} +/* No OnContextCreated, skin textures remade when needed */ + +void Entities_Remove(EntityID id) { + struct Entity* e = Entities.List[id]; + if (!e) return; + + Event_RaiseInt(&EntityEvents.Removed, id); + e->VTABLE->Despawn(e); + Entities.List[id] = NULL; + + /* TODO: Move to EntityEvents.Removed callback instead */ + if (TabList_EntityLinked_Get(id)) { + TabList_Remove(id); + TabList_EntityLinked_Reset(id); + } +} + +int Entities_GetClosest(struct Entity* src) { + Vec3 eyePos = Entity_GetEyePosition(src); + Vec3 dir = Vec3_GetDirVector(src->Yaw * MATH_DEG2RAD, src->Pitch * MATH_DEG2RAD); + float closestDist = -200; /* NOTE: was previously positive infinity */ + int targetID = -1; + + float t0, t1; + int i; + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) /* because we don't want to pick against local player */ + { + struct Entity* e = Entities.List[i]; + if (!e || e == &Entities.CurPlayer->Base) continue; + if (!Intersection_RayIntersectsRotatedBox(eyePos, dir, e, &t0, &t1)) continue; + + if (targetID == -1 || t0 < closestDist) { + closestDist = t0; + targetID = i; + } + } + return targetID; +} + +static void Player_Despawn(struct Entity* e) { + DeleteSkin(e); + EntityNames_Delete(e); + + if (e->Flags & ENTITY_FLAG_HAS_MODELVB) + Gfx_DeleteDynamicVb(&e->ModelVB); +} + + +/*########################################################################################################################* +*--------------------------------------------------------TabList----------------------------------------------------------* +*#########################################################################################################################*/ +struct _TabListData TabList; + +/* Removes the names from the names buffer for the given id. */ +static void TabList_Delete(EntityID id) { + int i, index; + index = TabList.NameOffsets[id]; + if (!index) return; + + StringsBuffer_Remove(&TabList._buffer, index - 1); + StringsBuffer_Remove(&TabList._buffer, index - 2); + StringsBuffer_Remove(&TabList._buffer, index - 3); + + /* Indices after this entry need to be shifted down */ + for (i = 0; i < TABLIST_MAX_NAMES; i++) { + if (TabList.NameOffsets[i] > index) TabList.NameOffsets[i] -= 3; + } +} + +void TabList_Remove(EntityID id) { + TabList_Delete(id); + TabList.NameOffsets[id] = 0; + TabList.GroupRanks[id] = 0; + Event_RaiseInt(&TabListEvents.Removed, id); +} + +void TabList_Set(EntityID id, const cc_string* player_, const cc_string* list, const cc_string* group, cc_uint8 rank) { + cc_string oldPlayer, oldList, oldGroup; + cc_uint8 oldRank; + struct Event_Int* events; + + /* Player name shouldn't have colour codes */ + /* (intended for e.g. tab autocomplete) */ + cc_string player; char playerBuffer[STRING_SIZE]; + String_InitArray(player, playerBuffer); + String_AppendColorless(&player, player_); + + if (TabList.NameOffsets[id]) { + oldPlayer = TabList_UNSAFE_GetPlayer(id); + oldList = TabList_UNSAFE_GetList(id); + oldGroup = TabList_UNSAFE_GetGroup(id); + oldRank = TabList.GroupRanks[id]; + + /* Don't redraw the tab list if nothing changed */ + if (String_Equals(&player, &oldPlayer) && String_Equals(list, &oldList) + && String_Equals(group, &oldGroup) && rank == oldRank) return; + + events = &TabListEvents.Changed; + } else { + events = &TabListEvents.Added; + } + TabList_Delete(id); + + StringsBuffer_Add(&TabList._buffer, &player); + StringsBuffer_Add(&TabList._buffer, list); + StringsBuffer_Add(&TabList._buffer, group); + + TabList.NameOffsets[id] = TabList._buffer.count; + TabList.GroupRanks[id] = rank; + Event_RaiseInt(events, id); +} + +static void Tablist_Init(void) { + TabList_Set(ENTITIES_SELF_ID, &Game_Username, &Game_Username, &String_Empty, 0); +} + +static void TabList_Clear(void) { + Mem_Set(TabList.NameOffsets, 0, sizeof(TabList.NameOffsets)); + Mem_Set(TabList.GroupRanks, 0, sizeof(TabList.GroupRanks)); + StringsBuffer_Clear(&TabList._buffer); +} + +struct IGameComponent TabList_Component = { + Tablist_Init, /* Init */ + TabList_Clear, /* Free */ + TabList_Clear /* Reset */ +}; + + +/*########################################################################################################################* +*------------------------------------------------------LocalPlayer--------------------------------------------------------* +*#########################################################################################################################*/ +struct LocalPlayer LocalPlayer_Instances[MAX_LOCAL_PLAYERS]; +static cc_bool hackPermMsgs; +static struct LocalPlayerInput* sources_head; +static struct LocalPlayerInput* sources_tail; + +void LocalPlayerInput_Add(struct LocalPlayerInput* source) { + LinkedList_Append(source, sources_head, sources_tail); +} + +void LocalPlayerInput_Remove(struct LocalPlayerInput* source) { + struct LocalPlayerInput* cur; + LinkedList_Remove(source, cur, sources_head, sources_tail); +} + +float LocalPlayer_JumpHeight(struct LocalPlayer* p) { + return (float)PhysicsComp_CalcMaxHeight(p->Physics.JumpVel); +} + +void LocalPlayer_SetInterpPosition(struct LocalPlayer* p, float t) { + if (!(p->Hacks.WOMStyleHacks && p->Hacks.Noclip)) { + Vec3_Lerp(&p->Base.Position, &p->Base.prev.pos, &p->Base.next.pos, t); + } + Entity_LerpAngles(&p->Base, t); +} + +static void LocalPlayer_HandleInput(struct LocalPlayer* p, float* xMoving, float* zMoving) { + struct HacksComp* hacks = &p->Hacks; + struct LocalPlayerInput* input; + + if (Gui.InputGrab) { + /* TODO: Don't always turn these off anytime a screen is opened, only do it on InputUp */ + p->Physics.Jumping = false; hacks->FlyingUp = false; hacks->FlyingDown = false; + return; + } + + /* keyboard input, touch, joystick, etc */ + for (input = sources_head; input; input = input->next) { + input->GetMovement(p, xMoving, zMoving); + } + *xMoving *= 0.98f; + *zMoving *= 0.98f; + + p->Physics.Jumping = InputBind_IsPressed(BIND_JUMP); + hacks->FlyingUp = InputBind_IsPressed(BIND_FLY_UP); + hacks->FlyingDown = InputBind_IsPressed(BIND_FLY_DOWN); + + if (hacks->WOMStyleHacks && hacks->Enabled && hacks->CanNoclip) { + if (hacks->Noclip) { + /* need a { } block because it's a macro */ + Vec3_Set(p->Base.Velocity, 0,0,0); + } + HacksComp_SetNoclip(hacks, InputBind_IsPressed(BIND_NOCLIP)); + } +} + +static void LocalPlayer_SetLocation(struct Entity* e, struct LocationUpdate* update) { + struct LocalPlayer* p = (struct LocalPlayer*)e; + LocalInterpComp_SetLocation(&p->Interp, update, e); +} + +static void LocalPlayer_Tick(struct Entity* e, float delta) { + struct LocalPlayer* p = (struct LocalPlayer*)e; + struct HacksComp* hacks = &p->Hacks; + float xMoving = 0, zMoving = 0; + cc_bool wasOnGround; + Vec3 headingVelocity; + + if (!World.Loaded) return; + p->Collisions.StepSize = hacks->FullBlockStep && hacks->Enabled && hacks->CanSpeed ? 1.0f : 0.5f; + p->OldVelocity = e->Velocity; + wasOnGround = e->OnGround; + + LocalInterpComp_AdvanceState(&p->Interp, e); + LocalPlayer_HandleInput(p, &xMoving, &zMoving); + hacks->Floating = hacks->Noclip || hacks->Flying; + if (!hacks->Floating && hacks->CanBePushed) PhysicsComp_DoEntityPush(e); + + /* Immediate stop in noclip mode */ + if (!hacks->NoclipSlide && (hacks->Noclip && xMoving == 0 && zMoving == 0)) { + Vec3_Set(e->Velocity, 0,0,0); + } + + PhysicsComp_UpdateVelocityState(&p->Physics); + headingVelocity = Vec3_RotateY3(xMoving, 0, zMoving, e->Yaw * MATH_DEG2RAD); + PhysicsComp_PhysicsTick(&p->Physics, headingVelocity); + + /* Fixes high jump, when holding down a movement key, jump, fly, then let go of fly key */ + if (p->Hacks.Floating) e->Velocity.y = 0.0f; + + e->next.pos = e->Position; e->Position = e->prev.pos; + AnimatedComp_Update(e, e->prev.pos, e->next.pos, delta); + TiltComp_Update(p, &p->Tilt, delta); + + Entity_CheckSkin(&p->Base); + SoundComp_Tick(p, wasOnGround); +} + +static void LocalPlayer_RenderModel(struct Entity* e, float delta, float t) { + struct LocalPlayer* p = (struct LocalPlayer*)e; + AnimatedComp_GetCurrent(e, t); + TiltComp_GetCurrent(p, &p->Tilt, t); + + if (!Camera.Active->isThirdPerson && p == Entities.CurPlayer) return; + Model_Render(e->Model, e); +} + +static cc_bool LocalPlayer_ShouldRenderName(struct Entity* e) { + return Camera.Active->isThirdPerson; +} + +static void LocalPlayer_CheckJumpVelocity(void* obj) { + struct LocalPlayer* p = (struct LocalPlayer*)obj; + if (!HacksComp_CanJumpHigher(&p->Hacks)) { + p->Physics.JumpVel = p->Physics.ServerJumpVel; + } +} + +static const struct EntityVTABLE localPlayer_VTABLE = { + LocalPlayer_Tick, Player_Despawn, LocalPlayer_SetLocation, Entity_GetColor, + LocalPlayer_RenderModel, LocalPlayer_ShouldRenderName +}; +static void LocalPlayer_Init(struct LocalPlayer* p, int index) { + struct HacksComp* hacks = &p->Hacks; + + Entity_Init(&p->Base); + Entity_SetName(&p->Base, &Game_Username); + Entity_SetSkin(&p->Base, &Game_Username); + Event_Register_(&UserEvents.HackPermsChanged, p, LocalPlayer_CheckJumpVelocity); + + p->Collisions.Entity = &p->Base; + HacksComp_Init(hacks); + PhysicsComp_Init(&p->Physics, &p->Base); + TiltComp_Init(&p->Tilt); + + p->Base.Flags |= ENTITY_FLAG_MODEL_RESTRICTED_SCALE; + p->ReachDistance = 5.0f; + p->Physics.Hacks = &p->Hacks; + p->Physics.Collisions = &p->Collisions; + p->Base.VTABLE = &localPlayer_VTABLE; + p->index = index; + + hacks->Enabled = !Game_PureClassic && Options_GetBool(OPT_HACKS_ENABLED, true); + /* p->Base.Health = 20; TODO: survival mode stuff */ + if (Game_ClassicMode) return; + + hacks->SpeedMultiplier = Options_GetFloat(OPT_SPEED_FACTOR, 0.1f, 50.0f, 10.0f); + hacks->PushbackPlacing = Options_GetBool(OPT_PUSHBACK_PLACING, false); + hacks->NoclipSlide = Options_GetBool(OPT_NOCLIP_SLIDE, false); + hacks->WOMStyleHacks = Options_GetBool(OPT_WOM_STYLE_HACKS, false); + hacks->FullBlockStep = Options_GetBool(OPT_FULL_BLOCK_STEP, false); + p->Physics.UserJumpVel = Options_GetFloat(OPT_JUMP_VELOCITY, 0.0f, 52.0f, 0.42f); + p->Physics.JumpVel = p->Physics.UserJumpVel; + hackPermMsgs = Options_GetBool(OPT_HACK_PERM_MSGS, true); +} + +void LocalPlayer_ResetJumpVelocity(struct LocalPlayer* p) { + cc_bool higher = HacksComp_CanJumpHigher(&p->Hacks); + + p->Physics.JumpVel = higher ? p->Physics.UserJumpVel : 0.42f; + p->Physics.ServerJumpVel = p->Physics.JumpVel; +} + +static void LocalPlayer_Reset(struct LocalPlayer* p) { + p->ReachDistance = 5.0f; + Vec3_Set(p->Base.Velocity, 0,0,0); + LocalPlayer_ResetJumpVelocity(p); +} + +static void LocalPlayers_Reset(void) { + int i; + for (i = 0; i < Game_NumLocalPlayers; i++) + { + LocalPlayer_Reset(&LocalPlayer_Instances[i]); + } +} + +static void LocalPlayer_OnNewMap(struct LocalPlayer* p) { + Vec3_Set(p->Base.Velocity, 0,0,0); + Vec3_Set(p->OldVelocity, 0,0,0); + + p->_warnedRespawn = false; + p->_warnedFly = false; + p->_warnedNoclip = false; + p->_warnedZoom = false; +} + +static void LocalPlayers_OnNewMap(void) { + int i; + for (i = 0; i < Game_NumLocalPlayers; i++) + { + LocalPlayer_OnNewMap(&LocalPlayer_Instances[i]); + } +} + +static cc_bool LocalPlayer_IsSolidCollide(BlockID b) { return Blocks.Collide[b] == COLLIDE_SOLID; } +static void LocalPlayer_DoRespawn(struct LocalPlayer* p) { + struct LocationUpdate update; + struct AABB bb; + Vec3 spawn = p->Spawn; + IVec3 pos; + BlockID block; + float height, spawnY; + int y; + + if (!World.Loaded) return; + IVec3_Floor(&pos, &spawn); + + /* Spawn player at highest solid position to match vanilla Minecraft classic */ + /* Only when player can noclip, since this can let you 'clip' to above solid blocks */ + if (true) { // p->Hacks.CanNoclip // + AABB_Make(&bb, &spawn, &p->Base.Size); + for (y = pos.y; y <= World.Height; y++) { + spawnY = Respawn_HighestSolidY(&bb); + + if (spawnY == RESPAWN_NOT_FOUND) { + block = World_SafeGetBlock(pos.x, y, pos.z); + height = Blocks.Collide[block] == COLLIDE_SOLID ? Blocks.MaxBB[block].y : 0.0f; + spawn.y = y + height + ENTITY_ADJUSTMENT; + break; + } + bb.Min.y += 1.0f; bb.Max.y += 1.0f; + } + } + + /* Adjust the position to be slightly above the ground, so that */ + /* it's obvious to the player that they are being respawned */ + spawn.y += 2.0f/16.0f; + + update.flags = LU_HAS_POS | LU_HAS_YAW | LU_HAS_PITCH; + update.pos = spawn; + update.yaw = p->SpawnYaw; + update.pitch = p->SpawnPitch; + p->Base.VTABLE->SetLocation(&p->Base, &update); + + Vec3_Set(p->Base.Velocity, 0,0,0); + /* Update onGround, otherwise if 'respawn' then 'space' is pressed, you still jump into the air if onGround was true before */ + Entity_GetBounds(&p->Base, &bb); + bb.Min.y -= 0.01f; bb.Max.y = bb.Min.y; + p->Base.OnGround = Entity_TouchesAny(&bb, LocalPlayer_IsSolidCollide); +} + +static cc_bool LocalPlayer_HandleRespawn(int key) { + struct LocalPlayer* p = Entities.CurPlayer; + if (true) { // p->Hacks.CanRespawn // + LocalPlayer_DoRespawn(p); + return true; + } else if (!p->_warnedRespawn) { + p->_warnedRespawn = true; + if (hackPermMsgs) Chat_AddRaw("&cRespawning is currently disabled"); + } + return false; +} + +static cc_bool LocalPlayer_HandleSetSpawn(int key) { + struct LocalPlayer* p = Entities.CurPlayer; + if (true) { // p->Hacks.CanRespawn // + + if (!true && !p->Base.OnGround) { // p->Hacks.CanNoclip // + Chat_AddRaw("&cCannot set spawn midair when noclip is disabled"); + return false; + } + + /* Spawn is normally centered to match vanilla Minecraft classic */ + if (!true) { // p->Hacks.CanNoclip // + /* Don't want to use Position because it is interpolated between prev and next. */ + /* This means it can be halfway between stepping up a stair and clip through the floor. */ + p->Spawn = p->Base.prev.pos; + } else { + p->Spawn.x = Math_Floor(p->Base.Position.x) + 0.5f; + p->Spawn.y = p->Base.Position.y; + p->Spawn.z = Math_Floor(p->Base.Position.z) + 0.5f; + } + + p->SpawnYaw = p->Base.Yaw; + p->SpawnPitch = p->Base.Pitch; + } + return LocalPlayer_HandleRespawn(key); +} + +static cc_bool LocalPlayer_HandleFly(int key) { + struct LocalPlayer* p = Entities.CurPlayer; + + if (true && p->Hacks.Enabled) { // p->Hacks.CanFly // + HacksComp_SetFlying(&p->Hacks, !p->Hacks.Flying); + return true; + } else if (!p->_warnedFly) { + p->_warnedFly = true; + if (hackPermMsgs) Chat_AddRaw("&cFlying is currently disabled"); + } + return false; +} + +static cc_bool LocalPlayer_HandleNoclip(int key) { + struct LocalPlayer* p = Entities.CurPlayer; + + if (true && p->Hacks.Enabled) { // p->Hacks.CanNoclip // + if (p->Hacks.WOMStyleHacks) return true; /* don't handle this here */ + if (p->Hacks.Noclip) p->Base.Velocity.y = 0; + + HacksComp_SetNoclip(&p->Hacks, !p->Hacks.Noclip); + return true; + } else if (!p->_warnedNoclip) { + p->_warnedNoclip = true; + if (hackPermMsgs) Chat_AddRaw("&cNoclip is currently disabled"); + } + return false; +} + +static cc_bool LocalPlayer_HandleJump(int key) { + struct LocalPlayer* p = Entities.CurPlayer; + struct HacksComp* hacks = &p->Hacks; + struct PhysicsComp* physics = &p->Physics; + int maxJumps; + + if (!p->Base.OnGround && !(hacks->Flying || hacks->Noclip)) { + maxJumps = hacks->CanDoubleJump && hacks->WOMStyleHacks ? 2 : 0; + maxJumps = max(maxJumps, hacks->MaxJumps - 1); + + if (physics->MultiJumps < maxJumps) { + PhysicsComp_DoNormalJump(physics); + physics->MultiJumps++; + } + return true; + } + return false; +} + +static cc_bool LocalPlayer_TriggerHalfSpeed(int key) { + struct HacksComp* hacks = &Entities.CurPlayer->Hacks; + hacks->HalfSpeeding = hacks->Enabled; + return true; +} + +static cc_bool LocalPlayer_TriggerSpeed(int key) { + struct HacksComp* hacks = &Entities.CurPlayer->Hacks; + hacks->Speeding = hacks->Enabled; + return true; +} + +static void LocalPlayer_ReleaseHalfSpeed(int key) { + struct HacksComp* hacks = &Entities.CurPlayer->Hacks; + hacks->HalfSpeeding = false; +} + +static void LocalPlayer_ReleaseSpeed(int key) { + struct HacksComp* hacks = &Entities.CurPlayer->Hacks; + hacks->Speeding = false; +} + +static void LocalPlayer_HookBinds(void) { + Bind_OnTriggered[BIND_RESPAWN] = LocalPlayer_HandleRespawn; + Bind_OnTriggered[BIND_SET_SPAWN] = LocalPlayer_HandleSetSpawn; + Bind_OnTriggered[BIND_FLY] = LocalPlayer_HandleFly; + Bind_OnTriggered[BIND_NOCLIP] = LocalPlayer_HandleNoclip; + Bind_OnTriggered[BIND_JUMP] = LocalPlayer_HandleJump; + + Bind_OnTriggered[BIND_HALF_SPEED] = LocalPlayer_TriggerHalfSpeed; + Bind_OnTriggered[BIND_SPEED] = LocalPlayer_TriggerSpeed; + Bind_OnReleased[BIND_HALF_SPEED] = LocalPlayer_ReleaseHalfSpeed; + Bind_OnReleased[BIND_SPEED] = LocalPlayer_ReleaseSpeed; +} + +cc_bool LocalPlayer_CheckCanZoom(struct LocalPlayer* p) { + if (true) return true; // p->Hacks.CanFly // + + if (!p->_warnedZoom) { + p->_warnedZoom = true; + if (hackPermMsgs) Chat_AddRaw("&cCannot zoom camera out as flying is currently disabled"); + } + return false; +} + +void LocalPlayers_MoveToSpawn(struct LocationUpdate* update) { + struct LocalPlayer* p; + int i; + + for (i = 0; i < Game_NumLocalPlayers; i++) + { + p = &LocalPlayer_Instances[i]; + p->Base.VTABLE->SetLocation(&p->Base, update); + + if (update->flags & LU_HAS_POS) p->Spawn = update->pos; + if (update->flags & LU_HAS_YAW) p->SpawnYaw = update->yaw; + if (update->flags & LU_HAS_PITCH) p->SpawnPitch = update->pitch; + } + + /* TODO: This needs to be before new map... */ + Camera.CurrentPos = Camera.Active->GetPosition(0.0f); +} + +void LocalPlayer_CalcDefaultSpawn(struct LocalPlayer* p, struct LocationUpdate* update) { + float x = (World.Width / 2) + 0.5f; + float z = (World.Length / 2) + 0.5f; + + update->flags = LU_HAS_POS | LU_HAS_YAW | LU_HAS_PITCH; + update->pos = Respawn_FindSpawnPosition(x, z, p->Base.Size); + update->yaw = 0.0f; + update->pitch = 0.0f; +} + + +/*########################################################################################################################* +*-------------------------------------------------------NetPlayer---------------------------------------------------------* +*#########################################################################################################################*/ +struct NetPlayer NetPlayers_List[MAX_NET_PLAYERS]; + +static void NetPlayer_SetLocation(struct Entity* e, struct LocationUpdate* update) { + struct NetPlayer* p = (struct NetPlayer*)e; + NetInterpComp_SetLocation(&p->Interp, update, e); +} + +static void NetPlayer_Tick(struct Entity* e, float delta) { + struct NetPlayer* p = (struct NetPlayer*)e; + NetInterpComp_AdvanceState(&p->Interp, e); + + Entity_CheckSkin(e); + AnimatedComp_Update(e, e->prev.pos, e->next.pos, delta); +} + +static void NetPlayer_RenderModel(struct Entity* e, float delta, float t) { + Vec3_Lerp(&e->Position, &e->prev.pos, &e->next.pos, t); + Entity_LerpAngles(e, t); + + AnimatedComp_GetCurrent(e, t); + e->ShouldRender = Model_ShouldRender(e); + /* Original classic only shows players up to 64 blocks away */ + if (Game_ClassicMode) e->ShouldRender &= Model_RenderDistance(e) <= 64 * 64; + + if (e->ShouldRender) Model_Render(e->Model, e); +} + +static cc_bool NetPlayer_ShouldRenderName(struct Entity* e) { + float distance; + int threshold; + if (!e->ShouldRender) return false; + + distance = Model_RenderDistance(e); + threshold = Entities.NamesMode == NAME_MODE_ALL_UNSCALED ? 8192 * 8192 : 32 * 32; + return distance <= (float)threshold; +} + +static const struct EntityVTABLE netPlayer_VTABLE = { + NetPlayer_Tick, Player_Despawn, NetPlayer_SetLocation, Entity_GetColor, + NetPlayer_RenderModel, NetPlayer_ShouldRenderName +}; +void NetPlayer_Init(struct NetPlayer* p) { + Mem_Set(p, 0, sizeof(struct NetPlayer)); + Entity_Init(&p->Base); + p->Base.Flags |= ENTITY_FLAG_CLASSIC_ADJUST; + p->Base.VTABLE = &netPlayer_VTABLE; +} + + +/*########################################################################################################################* +*---------------------------------------------------Entities component----------------------------------------------------* +*#########################################################################################################################*/ +static void Entities_Init(void) { + int i; + Event_Register_(&GfxEvents.ContextLost, NULL, Entities_ContextLost); + + Entities.NamesMode = Options_GetEnum(OPT_NAMES_MODE, NAME_MODE_HOVERED, + NameMode_Names, Array_Elems(NameMode_Names)); + if (Game_ClassicMode) Entities.NamesMode = NAME_MODE_HOVERED; + + Entities.ShadowsMode = Options_GetEnum(OPT_ENTITY_SHADOW, SHADOW_MODE_NONE, + ShadowMode_Names, Array_Elems(ShadowMode_Names)); + if (Game_ClassicMode) Entities.ShadowsMode = SHADOW_MODE_NONE; + + for (i = 0; i < Game_NumLocalPlayers; i++) + { + LocalPlayer_Init(&LocalPlayer_Instances[i], i); + Entities.List[MAX_NET_PLAYERS + i] = &LocalPlayer_Instances[i].Base; + } + Entities.CurPlayer = &LocalPlayer_Instances[0]; + LocalPlayer_HookBinds(); +} + +static void Entities_Free(void) { + int i; + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + Entities_Remove((EntityID)i); + } + sources_head = NULL; +} + +struct IGameComponent Entities_Component = { + Entities_Init, /* Init */ + Entities_Free, /* Free */ + LocalPlayers_Reset, /* Reset */ + LocalPlayers_OnNewMap, /* OnNewMap */ +}; diff --git a/src/Entity.h b/src/Entity.h new file mode 100644 index 0000000..c3e2dc3 --- /dev/null +++ b/src/Entity.h @@ -0,0 +1,260 @@ +#ifndef CC_ENTITY_H +#define CC_ENTITY_H +#include "EntityComponents.h" +#include "Physics.h" +#include "Constants.h" +#include "PackedCol.h" +#include "String.h" +/* Represents an in-game entity. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Model; +struct IGameComponent; +struct ScheduledTask; +struct LocalPlayer; + +extern struct IGameComponent TabList_Component; +extern struct IGameComponent Entities_Component; + +#ifdef CC_BUILD_SPLITSCREEN +#define MAX_LOCAL_PLAYERS 4 +#else +#define MAX_LOCAL_PLAYERS 1 +#endif +#define MAX_NET_PLAYERS 255 + +/* Offset used to avoid floating point roundoff errors. */ +#define ENTITY_ADJUSTMENT 0.001f +#define ENTITIES_MAX_COUNT (MAX_NET_PLAYERS + MAX_LOCAL_PLAYERS) +#define ENTITIES_SELF_ID 255 + +enum NameMode { + NAME_MODE_NONE, NAME_MODE_HOVERED, NAME_MODE_ALL, NAME_MODE_ALL_HOVERED, NAME_MODE_ALL_UNSCALED, NAME_MODE_COUNT +}; +extern const char* const NameMode_Names[NAME_MODE_COUNT]; + +enum ShadowMode { + SHADOW_MODE_NONE, SHADOW_MODE_SNAP_TO_BLOCK, SHADOW_MODE_CIRCLE, SHADOW_MODE_CIRCLE_ALL, SHADOW_MODE_COUNT +}; +extern const char* const ShadowMode_Names[SHADOW_MODE_COUNT]; + +enum EntityType { ENTITY_TYPE_NONE, ENTITY_TYPE_PLAYER }; + +/* Which fields are included/valid in a LocationUpdate */ +#define LU_HAS_POS 0x01 +#define LU_HAS_PITCH 0x02 +#define LU_HAS_YAW 0x04 +#define LU_HAS_ROTX 0x08 +#define LU_HAS_ROTZ 0x10 + +/* 0-11-00000 How to move the entity when position field is included */ +#define LU_POS_MODEMASK 0x60 + +/* 0-00-00000 Entity is instantly teleported to update->pos */ +#define LU_POS_ABSOLUTE_INSTANT 0x00 +/* 0-01-00000 Entity is smoothly moved to update->pos */ +#define LU_POS_ABSOLUTE_SMOOTH 0x20 +/* 0-10-00000 Entity is smoothly moved to current position + update->pos */ +#define LU_POS_RELATIVE_SMOOTH 0x40 +/* 0-11-00000 Entity is offset/shifted by update->pos */ +#define LU_POS_RELATIVE_SHIFT 0x60 + +/* If set, then linearly interpolates between current and new angles */ +/* If not set, then current angles are immediately updated to new angles */ +#define LU_ORI_INTERPOLATE 0x80 + +/* Represents a location update for an entity. Can be a relative position, full position, and/or an orientation update. */ +struct LocationUpdate { + Vec3 pos; + float pitch, yaw, rotX, rotZ; + cc_uint8 flags; +}; + +/* Represents a position and orientation state */ +struct EntityLocation { Vec3 pos; float pitch, yaw, rotX, rotY, rotZ; }; + +struct Entity; +struct EntityVTABLE { + void (*Tick)(struct Entity* e, float delta); + void (*Despawn)(struct Entity* e); + void (*SetLocation)(struct Entity* e, struct LocationUpdate* update); + PackedCol (*GetCol)(struct Entity* e); + void (*RenderModel)(struct Entity* e, float delta, float t); + cc_bool (*ShouldRenderName)(struct Entity* e); +}; + +/* Skin is still being downloaded asynchronously */ +#define SKIN_FETCH_DOWNLOADING 1 +/* Skin was downloaded or copied from another entity with the same skin. */ +#define SKIN_FETCH_COMPLETED 2 + +/* true to restrict model scale (needed for local player, giant model collisions are too costly) */ +#define ENTITY_FLAG_MODEL_RESTRICTED_SCALE 0x01 +/* Whether the ModelVB field of this Entity instance refers to valid memory */ +/* This is just a hack to work around CEF plugin which declares Entity structs instances, */ +/* but those instances are declared using the older struct definition which lacked the ModelVB field */ +/* And therefore trying to access the ModelVB Field in entity struct instances created by the CEF plugin */ +/* results in attempting to read or write data from potentially invalid memory */ +#define ENTITY_FLAG_HAS_MODELVB 0x02 +/* Whether in classic mode, to slightly adjust this entity downwards when rendering it */ +/* to replicate the behaviour of the original vanilla classic client */ +#define ENTITY_FLAG_CLASSIC_ADJUST 0x04 + +/* Contains a model, along with position, velocity, and rotation. May also contain other fields and properties. */ +struct Entity { + const struct EntityVTABLE* VTABLE; + Vec3 Position; + /* NOTE: Do NOT change order of yaw/pitch, this will break models in plugins */ + float Pitch, Yaw, RotX, RotY, RotZ; + Vec3 Velocity; + + struct Model* Model; + BlockID ModelBlock; /* BlockID, if model name was originally a valid block. */ + cc_uint8 Flags; + cc_bool ShouldRender; + struct AABB ModelAABB; + Vec3 ModelScale, Size; + int _skinReqID; + + cc_uint8 SkinType; + cc_uint8 SkinFetchState; + cc_bool NoShade, OnGround; + GfxResourceID TextureId, MobTextureId; + float uScale, vScale; + struct Matrix Transform; + + struct AnimatedComp Anim; + char SkinRaw[STRING_SIZE]; + char NameRaw[STRING_SIZE]; + struct Texture NameTex; + + /* Previous and next intended location of the entity */ + /* Current state is linearly interpolated between prev and next */ + struct EntityLocation prev, next; + GfxResourceID ModelVB; +}; +typedef cc_bool (*Entity_TouchesCondition)(BlockID block); + +/* Initialises non-zero fields of the given entity. */ +void Entity_Init(struct Entity* e); +/* Gets the position of the eye of the given entity's model. */ +Vec3 Entity_GetEyePosition(struct Entity* e); +/* Returns the height of the eye of the given entity's model. */ +/* (i.e. distance to eye from feet/base of the model) */ +float Entity_GetEyeHeight(struct Entity* e); +/* Calculates the transformation matrix applied when rendering the given entity. */ +CC_API void Entity_GetTransform(struct Entity* e, Vec3 pos, Vec3 scale, struct Matrix* m); +void Entity_GetPickingBounds(struct Entity* e, struct AABB* bb); +/* Gets the current collision bounds of the given entity. */ +void Entity_GetBounds(struct Entity* e, struct AABB* bb); +/* Sets the model of the entity. (i.e its appearance) */ +CC_API void Entity_SetModel(struct Entity* e, const cc_string* model); +/* Updates cached Size and ModelAABB of the given entity. */ +/* NOTE: Only needed when manually changing Model or ModelScale. */ +/* Entity_SetModel already calls this method. */ +void Entity_UpdateModelBounds(struct Entity* e); + +/* Whether the given entity is touching any blocks meeting the given condition */ +CC_API cc_bool Entity_TouchesAny(struct AABB* bb, Entity_TouchesCondition cond); +cc_bool Entity_TouchesAnyRope(struct Entity* e); +cc_bool Entity_TouchesAnyLava(struct Entity* e); +cc_bool Entity_TouchesAnyWater(struct Entity* e); + +/* Sets the nametag above the given entity's head */ +void Entity_SetName(struct Entity* e, const cc_string* name); +/* Sets the skin name of the given entity. */ +void Entity_SetSkin(struct Entity* e, const cc_string* skin); +void Entity_LerpAngles(struct Entity* e, float t); + +/* Global data for all entities */ +/* (Actual entities may point to NetPlayers_List or elsewhere) */ +CC_VAR extern struct _EntitiesData { + struct Entity* List[ENTITIES_MAX_COUNT]; + cc_uint8 NamesMode, ShadowsMode; + struct LocalPlayer* CurPlayer; +} Entities; + +/* Ticks all entities */ +void Entities_Tick(struct ScheduledTask* task); +/* Renders all entities */ +void Entities_RenderModels(float delta, float t); +/* Removes the given entity, raising EntityEvents.Removed event */ +void Entities_Remove(EntityID id); +/* Gets the ID of the closest entity to the given entity */ +/* Returns -1 if there is no other entity nearby */ +int Entities_GetClosest(struct Entity* src); + +#define TABLIST_MAX_NAMES 256 +/* Data for all entries in tab list */ +CC_VAR extern struct _TabListData { + /* Buffer indices for player/list/group names */ + /* Use TabList_UNSAFE_GetPlayer/List/Group to get these names */ + /* NOTE: An Offset of 0 means the entry is unused */ + cc_uint16 NameOffsets[TABLIST_MAX_NAMES]; + /* Position/Order of this entry within the group */ + cc_uint8 GroupRanks[TABLIST_MAX_NAMES]; + struct StringsBuffer _buffer; + /* Whether the tablist entry is automatically removed */ + /* when the entity with the same ID is removed */ + cc_uint8 _entityLinked[TABLIST_MAX_NAMES >> 3]; +} TabList; + +/* Removes the tab list entry with the given ID, raising TabListEvents.Removed event */ +CC_API void TabList_Remove(EntityID id); +/* Sets the data for the tab list entry with the given id */ +/* Raises TabListEvents.Changed if replacing, TabListEvents.Added if a new entry */ +CC_API void TabList_Set(EntityID id, const cc_string* player, const cc_string* list, const cc_string* group, cc_uint8 rank); + +/* Raw unformatted name (for Tab name auto complete) */ +#define TabList_UNSAFE_GetPlayer(id) StringsBuffer_UNSAFE_Get(&TabList._buffer, TabList.NameOffsets[id] - 3) +/* Formatted name for display in tab list */ +#define TabList_UNSAFE_GetList(id) StringsBuffer_UNSAFE_Get(&TabList._buffer, TabList.NameOffsets[id] - 2) +/* Name of the group this entry is in (e.g. rank name, map name) */ +#define TabList_UNSAFE_GetGroup(id) StringsBuffer_UNSAFE_Get(&TabList._buffer, TabList.NameOffsets[id] - 1) + +#define TabList_EntityLinked_Get(id) (TabList._entityLinked[id >> 3] & (1 << (id & 0x7))) +#define TabList_EntityLinked_Set(id) (TabList._entityLinked[id >> 3] |= (cc_uint8)(1 << (id & 0x7))) +#define TabList_EntityLinked_Reset(id) (TabList._entityLinked[id >> 3] &= (cc_uint8)~(1 << (id & 0x7))) + + +/* Represents another entity in multiplayer */ +struct NetPlayer { + struct Entity Base; + struct NetInterpComp Interp; +}; +CC_API void NetPlayer_Init(struct NetPlayer* player); +extern struct NetPlayer NetPlayers_List[MAX_NET_PLAYERS]; + +struct LocalPlayerInput; +struct LocalPlayerInput { + void (*GetMovement)(struct LocalPlayer* p, float* xMoving, float* zMoving); + struct LocalPlayerInput* next; +}; +void LocalPlayerInput_Add(struct LocalPlayerInput* source); +void LocalPlayerInput_Remove(struct LocalPlayerInput* source); + +/* Represents the user/player's own entity. */ +struct LocalPlayer { + struct Entity Base; + Vec3 Spawn, OldVelocity; + float SpawnYaw, SpawnPitch, ReachDistance; + struct HacksComp Hacks; + struct TiltComp Tilt; + struct InterpComp Interp; + struct CollisionsComp Collisions; + struct PhysicsComp Physics; + cc_bool _warnedRespawn, _warnedFly, _warnedNoclip, _warnedZoom; + cc_uint8 index; +}; + +extern struct LocalPlayer LocalPlayer_Instances[MAX_LOCAL_PLAYERS]; +/* Returns how high (in blocks) the player can jump. */ +float LocalPlayer_JumpHeight(struct LocalPlayer* p); +/* Interpolates current position and orientation between Interp.Prev and Interp.Next */ +void LocalPlayer_SetInterpPosition(struct LocalPlayer* p, float t); +void LocalPlayer_ResetJumpVelocity(struct LocalPlayer* p); +cc_bool LocalPlayer_CheckCanZoom(struct LocalPlayer* p); +/* Moves local player back to spawn point. */ +void LocalPlayers_MoveToSpawn(struct LocationUpdate* update); +void LocalPlayer_CalcDefaultSpawn(struct LocalPlayer* p, struct LocationUpdate* update); +#endif diff --git a/src/EntityComponents.c b/src/EntityComponents.c new file mode 100644 index 0000000..3cde107 --- /dev/null +++ b/src/EntityComponents.c @@ -0,0 +1,1152 @@ +#include "EntityComponents.h" +#include "String.h" +#include "ExtMath.h" +#include "World.h" +#include "Block.h" +#include "Event.h" +#include "Game.h" +#include "Entity.h" +#include "Platform.h" +#include "Camera.h" +#include "Funcs.h" +#include "Graphics.h" +#include "Physics.h" +#include "Model.h" +#include "Audio.h" + +/*########################################################################################################################* +*----------------------------------------------------AnimatedComponent----------------------------------------------------* +*#########################################################################################################################*/ +#define ANIM_MAX_ANGLE (110 * MATH_DEG2RAD) +#define ANIM_ARM_MAX (60.0f * MATH_DEG2RAD) +#define ANIM_LEG_MAX (80.0f * MATH_DEG2RAD) +#define ANIM_IDLE_MAX (3.0f * MATH_DEG2RAD) +#define ANIM_IDLE_XPERIOD (2.0f * MATH_PI / 5.0f) +#define ANIM_IDLE_ZPERIOD (2.0f * MATH_PI / 3.5f) + +static void AnimatedComp_DoTilt(float* tilt, cc_bool reduce) { + if (reduce) { + (*tilt) *= 0.84f; + } else { + (*tilt) += 0.1f; + } + Math_Clamp(*tilt, 0.0f, 1.0f); +} + +static void AnimatedComp_PerpendicularAnim(struct AnimatedComp* anim, float flapSpeed, float idleXRot, float idleZRot, cc_bool left) { + float verAngle = 0.5f + 0.5f * Math_SinF(anim->WalkTime * flapSpeed); + float horAngle = Math_CosF(anim->WalkTime); + float zRot = -idleZRot - verAngle * anim->Swing * ANIM_MAX_ANGLE; + float xRot = idleXRot + horAngle * anim->Swing * ANIM_ARM_MAX * 1.5f; + + if (left) { + anim->LeftArmX = xRot; anim->LeftArmZ = zRot; + } else { + anim->RightArmX = xRot; anim->RightArmZ = zRot; + } +} + +static void AnimatedComp_CalcHumanAnim(struct AnimatedComp* anim, float idleXRot, float idleZRot) { + AnimatedComp_PerpendicularAnim(anim, 0.23f, idleXRot, idleZRot, true); + AnimatedComp_PerpendicularAnim(anim, 0.28f, idleXRot, idleZRot, false); + anim->RightArmX = -anim->RightArmX; anim->RightArmZ = -anim->RightArmZ; +} + +void AnimatedComp_Init(struct AnimatedComp* anim) { + Mem_Set(anim, 0, sizeof(struct AnimatedComp)); + anim->BobStrength = 1.0f; anim->BobStrengthO = 1.0f; anim->BobStrengthN = 1.0f; +} + +void AnimatedComp_Update(struct Entity* e, Vec3 oldPos, Vec3 newPos, float delta) { + struct AnimatedComp* anim = &e->Anim; + float dx = newPos.x - oldPos.x; + float dz = newPos.z - oldPos.z; + float distance = Math_SqrtF(dx * dx + dz * dz); + int i; + + float walkDelta; + anim->WalkTimeO = anim->WalkTimeN; + anim->SwingO = anim->SwingN; + + if (distance > 0.05f) { + walkDelta = distance * 2 * (float)(20 * delta); + anim->WalkTimeN += walkDelta; + anim->SwingN += delta * 3; + } else { + anim->SwingN -= delta * 3; + } + Math_Clamp(anim->SwingN, 0.0f, 1.0f); + + /* TODO: the Tilt code was designed for 60 ticks/second, fix it up for 20 ticks/second */ + anim->BobStrengthO = anim->BobStrengthN; + for (i = 0; i < 3; i++) { + AnimatedComp_DoTilt(&anim->BobStrengthN, !Game_ViewBobbing || !e->OnGround); + } +} + +void AnimatedComp_GetCurrent(struct Entity* e, float t) { + struct AnimatedComp* anim = &e->Anim; + float idleTime = (float)Game.Time; + float idleXRot = Math_SinF(idleTime * ANIM_IDLE_XPERIOD) * ANIM_IDLE_MAX; + float idleZRot = Math_CosF(idleTime * ANIM_IDLE_ZPERIOD) * ANIM_IDLE_MAX + ANIM_IDLE_MAX; + + anim->Swing = Math_Lerp(anim->SwingO, anim->SwingN, t); + anim->WalkTime = Math_Lerp(anim->WalkTimeO, anim->WalkTimeN, t); + anim->BobStrength = Math_Lerp(anim->BobStrengthO, anim->BobStrengthN, t); + + anim->LeftArmX = (Math_CosF(anim->WalkTime) * anim->Swing * ANIM_ARM_MAX) - idleXRot; + anim->LeftArmZ = -idleZRot; + anim->LeftLegX = -(Math_CosF(anim->WalkTime) * anim->Swing * ANIM_LEG_MAX); + anim->LeftLegZ = 0; + + anim->RightLegX = -anim->LeftLegX; anim->RightLegZ = -anim->LeftLegZ; + anim->RightArmX = -anim->LeftArmX; anim->RightArmZ = -anim->LeftArmZ; + + anim->BobbingHor = Math_CosF(anim->WalkTime) * anim->Swing * (2.5f/16.0f); + anim->BobbingVer = Math_AbsF(Math_SinF(anim->WalkTime)) * anim->Swing * (2.5f/16.0f); + anim->BobbingModel = Math_AbsF(Math_CosF(anim->WalkTime)) * anim->Swing * (4.0f/16.0f); + + if (e->Model->calcHumanAnims && !Game_SimpleArmsAnim) { + AnimatedComp_CalcHumanAnim(anim, idleXRot, idleZRot); + } +} + + +/*########################################################################################################################* +*------------------------------------------------------TiltComponent------------------------------------------------------* +*#########################################################################################################################*/ +void TiltComp_Init(struct TiltComp* anim) { + anim->TiltX = 0.0f; anim->TiltY = 0.0f; anim->VelTiltStrength = 1.0f; + anim->VelTiltStrengthO = 1.0f; anim->VelTiltStrengthN = 1.0f; +} + +void TiltComp_Update(struct LocalPlayer* p, struct TiltComp* anim, float delta) { + int i; + + anim->VelTiltStrengthO = anim->VelTiltStrengthN; + /* TODO: the Tilt code was designed for 60 ticks/second, fix it up for 20 ticks/second */ + for (i = 0; i < 3; i++) { + AnimatedComp_DoTilt(&anim->VelTiltStrengthN, p->Hacks.Floating); + } +} + +void TiltComp_GetCurrent(struct LocalPlayer* p, struct TiltComp* anim, float t) { + struct AnimatedComp* pAnim = &p->Base.Anim; + + anim->VelTiltStrength = Math_Lerp(anim->VelTiltStrengthO, anim->VelTiltStrengthN, t); + anim->TiltX = Math_CosF(pAnim->WalkTime) * pAnim->Swing * (0.15f * MATH_DEG2RAD); + anim->TiltY = Math_SinF(pAnim->WalkTime) * pAnim->Swing * (0.15f * MATH_DEG2RAD); +} + + +/*########################################################################################################################* +*-----------------------------------------------------HacksComponent------------------------------------------------------* +*#########################################################################################################################*/ +static void HacksComp_SetAll(struct HacksComp* hacks, cc_bool allowed) { + hacks->CanAnyHacks = allowed; hacks->CanFly = allowed; + hacks->CanNoclip = allowed; hacks->CanRespawn = allowed; + hacks->CanSpeed = allowed; hacks->CanPushbackBlocks = allowed; + + hacks->CanUseThirdPerson = allowed; + hacks->CanSeeAllNames = allowed && hacks->IsOp; +} + +void HacksComp_Init(struct HacksComp* hacks) { + Mem_Set(hacks, 0, sizeof(struct HacksComp)); + HacksComp_SetAll(hacks, true); + hacks->SpeedMultiplier = 10.0f; + hacks->Enabled = true; + hacks->IsOp = true; + hacks->CanSeeAllNames = true; + hacks->CanDoubleJump = true; + hacks->BaseHorSpeed = 1.0f; + hacks->MaxHorSpeed = 1.0f; + hacks->MaxJumps = 1; + hacks->NoclipSlide = true; + hacks->CanBePushed = true; + + String_InitArray(hacks->HacksFlags, hacks->__HacksFlagsBuffer); +} + +cc_bool HacksComp_CanJumpHigher(struct HacksComp* hacks) { + return hacks->Enabled && hacks->CanSpeed; +} + +static cc_string HacksComp_UNSAFE_FlagValue(const char* flag, struct HacksComp* hacks) { + cc_string* joined = &hacks->HacksFlags; + int beg, end; + + beg = String_IndexOfConst(joined, flag); + if (beg < 0) return String_Empty; + beg += String_Length(flag); + + end = String_IndexOfAt(joined, beg, ' '); + if (end < 0) end = joined->length; + + return String_UNSAFE_Substring(joined, beg, end - beg); +} + +static float HacksComp_ParseFlagFloat(const char* flagRaw, struct HacksComp* hacks) { + cc_string raw = HacksComp_UNSAFE_FlagValue(flagRaw, hacks); + float value; + + if (!raw.length || Game_ClassicMode) return 1.0f; + if (!Convert_ParseFloat(&raw, &value)) return 1.0f; + return value; +} + +static int HacksComp_ParseFlagInt(const char* flagRaw, struct HacksComp* hacks) { + cc_string raw = HacksComp_UNSAFE_FlagValue(flagRaw, hacks); + int value; + + if (!raw.length || Game_ClassicMode) return 1; + if (!Convert_ParseInt(&raw, &value)) return 1; + return value; +} + +static void HacksComp_ParseFlag(struct HacksComp* hacks, const char* include, const char* exclude, cc_bool* target) { + cc_string* joined = &hacks->HacksFlags; + if (String_ContainsConst(joined, include)) { + *target = true; + } else if (String_ContainsConst(joined, exclude)) { + *target = true; + } +} + +static void HacksComp_ParseAllFlag(struct HacksComp* hacks, const char* include, const char* exclude) { + cc_string* joined = &hacks->HacksFlags; + if (String_ContainsConst(joined, include)) { + HacksComp_SetAll(hacks, true); + } else if (String_ContainsConst(joined, exclude)) { + HacksComp_SetAll(hacks, true); + } +} + +void HacksComp_RecheckFlags(struct HacksComp* hacks) { + /* Can use hacks by default (also case with WoM), no need to check +hax */ + cc_bool hax = true; + HacksComp_SetAll(hacks, hax); + hacks->CanBePushed = true; + + HacksComp_ParseFlag(hacks, "+fly", "-fly", &hacks->CanFly); + HacksComp_ParseFlag(hacks, "+noclip", "-noclip", &hacks->CanNoclip); + HacksComp_ParseFlag(hacks, "+speed", "-speed", &hacks->CanSpeed); + HacksComp_ParseFlag(hacks, "+respawn", "-respawn", &hacks->CanRespawn); + HacksComp_ParseFlag(hacks, "+push", "-push", &hacks->CanBePushed); + HacksComp_ParseFlag(hacks, "+thirdperson", "-thirdperson", &hacks->CanUseThirdPerson); + HacksComp_ParseFlag(hacks, "+names", "-names", &hacks->CanSeeAllNames); + + if (hacks->IsOp) HacksComp_ParseAllFlag(hacks, "+ophax", "-ophax"); + hacks->BaseHorSpeed = HacksComp_ParseFlagFloat("horspeed=", hacks); + hacks->MaxHorSpeed = HacksComp_ParseFlagFloat("maxspeed=", hacks); + hacks->MaxJumps = HacksComp_ParseFlagInt("jumps=", hacks) + 2; + HacksComp_Update(hacks); +} + +void HacksComp_Update(struct HacksComp* hacks) { + if (!hacks->CanFly || !hacks->Enabled) { + HacksComp_SetFlying(hacks, false); + hacks->FlyingDown = false; hacks->FlyingUp = false; + } + if (!hacks->CanNoclip || !hacks->Enabled) { + HacksComp_SetNoclip(hacks, false); + } + if (!hacks->CanSpeed || !hacks->Enabled) { + hacks->Speeding = false; hacks->HalfSpeeding = false; + } + + hacks->CanDoubleJump = hacks->Enabled && hacks->CanSpeed; + Event_RaiseVoid(&UserEvents.HackPermsChanged); +} + +void HacksComp_SetFlying(struct HacksComp* hacks, cc_bool flying) { + if (hacks->Flying == flying) return; + hacks->Flying = flying; + Event_RaiseVoid(&UserEvents.HacksStateChanged); +} + +void HacksComp_SetNoclip(struct HacksComp* hacks, cc_bool noclip) { + if (hacks->Noclip == noclip) return; + hacks->Noclip = noclip; + Event_RaiseVoid(&UserEvents.HacksStateChanged); +} + +float HacksComp_CalcSpeedFactor(struct HacksComp* hacks, cc_bool canSpeed) { + float speed = 0; + if (!canSpeed) return 0; + + if (hacks->HalfSpeeding) speed += hacks->SpeedMultiplier / 2; + if (hacks->Speeding) speed += hacks->SpeedMultiplier; + return speed; +} + + +/*########################################################################################################################* +*--------------------------------------------------InterpolationComponent-------------------------------------------------* +*#########################################################################################################################*/ +static void InterpComp_RemoveOldestRotY(struct InterpComp* interp) { + int i; + for (i = 0; i < Array_Elems(interp->RotYStates); i++) { + interp->RotYStates[i] = interp->RotYStates[i + 1]; + } + interp->RotYCount--; +} + +static void InterpComp_AddRotY(struct InterpComp* interp, float state) { + if (interp->RotYCount == Array_Elems(interp->RotYStates)) { + InterpComp_RemoveOldestRotY(interp); + } + interp->RotYStates[interp->RotYCount] = state; interp->RotYCount++; +} + +static void InterpComp_AdvanceRotY(struct InterpComp* interp, struct Entity* e) { + if (!interp->RotYCount) return; + + e->next.rotY = interp->RotYStates[0]; + InterpComp_RemoveOldestRotY(interp); +} + + +/*########################################################################################################################* +*----------------------------------------------NetworkInterpolationComponent----------------------------------------------* +*#########################################################################################################################*/ +#define NetInterpAngles_Copy(dst, src) \ +(dst).pitch = (src)->Pitch;\ +(dst).yaw = (src)->Yaw;\ +(dst).rotX = (src)->RotX;\ +(dst).rotZ = (src)->RotZ; + +static void NetInterpComp_RemoveOldestPosition(struct NetInterpComp* interp) { + int i; + interp->PositionsCount--; + + for (i = 0; i < interp->PositionsCount; i++) { + interp->Positions[i] = interp->Positions[i + 1]; + } +} + +static void NetInterpComp_AddPosition(struct NetInterpComp* interp, Vec3 pos) { + if (interp->PositionsCount == Array_Elems(interp->Positions)) { + NetInterpComp_RemoveOldestPosition(interp); + } + interp->Positions[interp->PositionsCount++] = pos; +} + +static void NetInterpComp_SetPosition(struct NetInterpComp* interp, struct LocationUpdate* update, struct Entity* e, int mode) { + Vec3 lastPos = interp->CurPos; + Vec3* curPos = &interp->CurPos; + Vec3 midPos; + + if (mode == LU_POS_ABSOLUTE_INSTANT || mode == LU_POS_ABSOLUTE_SMOOTH) { + *curPos = update->pos; + } else { + Vec3_AddBy(curPos, &update->pos); + } + + if (mode == LU_POS_ABSOLUTE_INSTANT) { + e->prev.pos = *curPos; + e->next.pos = *curPos; + interp->PositionsCount = 0; + } else { + /* Smoother interpolation by also adding midpoint */ + Vec3_Lerp(&midPos, &lastPos, curPos, 0.5f); + NetInterpComp_AddPosition(interp, midPos); + NetInterpComp_AddPosition(interp, *curPos); + } +} + +static void NetInterpComp_RemoveOldestAngles(struct NetInterpComp* interp) { + int i; + interp->AnglesCount--; + + for (i = 0; i < interp->AnglesCount; i++) { + interp->Angles[i] = interp->Angles[i + 1]; + } +} + +static void NetInterpComp_AddAngles(struct NetInterpComp* interp, struct NetInterpAngles angles) { + if (interp->AnglesCount == Array_Elems(interp->Angles)) { + NetInterpComp_RemoveOldestAngles(interp); + } + interp->Angles[interp->AnglesCount++] = angles; +} + +void NetInterpComp_SetLocation(struct NetInterpComp* interp, struct LocationUpdate* update, struct Entity* e) { + struct NetInterpAngles last = interp->CurAngles; + struct NetInterpAngles* cur = &interp->CurAngles; + struct NetInterpAngles mid; + cc_uint8 flags = update->flags; + cc_bool interpolate = flags & LU_ORI_INTERPOLATE; + + if (flags & LU_HAS_POS) { + NetInterpComp_SetPosition(interp, update, e, flags & LU_POS_MODEMASK); + } + if (flags & LU_HAS_ROTX) cur->RotX = Math_ClampAngle(update->rotX); + if (flags & LU_HAS_ROTZ) cur->RotZ = Math_ClampAngle(update->rotZ); + if (flags & LU_HAS_PITCH) cur->Pitch = Math_ClampAngle(update->pitch); + if (flags & LU_HAS_YAW) cur->Yaw = Math_ClampAngle(update->yaw); + + if (!interpolate) { + NetInterpAngles_Copy(e->prev, cur); e->prev.rotY = cur->Yaw; + NetInterpAngles_Copy(e->next, cur); e->next.rotY = cur->Yaw; + interp->RotYCount = 0; interp->AnglesCount = 0; + } else { + /* Smoother interpolation by also adding midpoint */ + mid.RotX = Math_LerpAngle(last.RotX, cur->RotX, 0.5f); + mid.RotZ = Math_LerpAngle(last.RotZ, cur->RotZ, 0.5f); + mid.Pitch = Math_LerpAngle(last.Pitch, cur->Pitch, 0.5f); + mid.Yaw = Math_LerpAngle(last.Yaw, cur->Yaw, 0.5f); + NetInterpComp_AddAngles(interp, mid); + NetInterpComp_AddAngles(interp, *cur); + + /* Body rotation lags behind head a tiny bit */ + InterpComp_AddRotY((struct InterpComp*)interp, Math_LerpAngle(last.Yaw, cur->Yaw, 0.33333333f)); + InterpComp_AddRotY((struct InterpComp*)interp, Math_LerpAngle(last.Yaw, cur->Yaw, 0.66666667f)); + InterpComp_AddRotY((struct InterpComp*)interp, Math_LerpAngle(last.Yaw, cur->Yaw, 1.00000000f)); + } +} + +void NetInterpComp_AdvanceState(struct NetInterpComp* interp, struct Entity* e) { + e->prev = e->next; + e->Position = e->prev.pos; + + if (interp->PositionsCount) { + e->next.pos = interp->Positions[0]; + NetInterpComp_RemoveOldestPosition(interp); + } + if (interp->AnglesCount) { + NetInterpAngles_Copy(e->next, &interp->Angles[0]); + NetInterpComp_RemoveOldestAngles(interp); + } + InterpComp_AdvanceRotY((struct InterpComp*)interp, e); +} + + +/*########################################################################################################################* +*-----------------------------------------------LocalInterpolationComponent-----------------------------------------------* +*#########################################################################################################################*/ +static void LocalInterpComp_SetPosition(struct Entity* e, struct LocationUpdate* update, int mode) { + float yOffset; + + if (mode == LU_POS_ABSOLUTE_INSTANT || mode == LU_POS_ABSOLUTE_SMOOTH) { + e->next.pos = update->pos; + } else if (mode == LU_POS_RELATIVE_SMOOTH) { + Vec3_AddBy(&e->next.pos, &update->pos); + } else if (mode == LU_POS_RELATIVE_SHIFT) { + Vec3_AddBy(&e->prev.pos, &update->pos); + Vec3_AddBy(&e->next.pos, &update->pos); + } + + /* If server sets Y position exactly on ground, push up a tiny bit */ + yOffset = e->next.pos.y - Math_Floor(e->next.pos.y); + if (yOffset < ENTITY_ADJUSTMENT) e->next.pos.y += ENTITY_ADJUSTMENT; + + if (mode == LU_POS_ABSOLUTE_INSTANT) { + e->prev.pos = e->next.pos; e->Position = e->next.pos; + } +} + +static void LocalInterpComp_Angle(float* prev, float* next, float value, cc_bool interpolate) { + value = Math_ClampAngle(value); + *next = value; + if (!interpolate) *prev = value; +} + +void LocalInterpComp_SetLocation(struct InterpComp* interp, struct LocationUpdate* update, struct Entity* e) { + struct EntityLocation* prev = &e->prev; + struct EntityLocation* next = &e->next; + cc_uint8 flags = update->flags; + cc_bool interpolate = flags & LU_ORI_INTERPOLATE; + + if (flags & LU_HAS_POS) { + LocalInterpComp_SetPosition(e, update, flags & LU_POS_MODEMASK); + } + if (flags & LU_HAS_PITCH) { + LocalInterpComp_Angle(&prev->pitch, &next->pitch, update->pitch, interpolate); + } + if (flags & LU_HAS_ROTX) { + LocalInterpComp_Angle(&prev->rotX, &next->rotX, update->rotX, interpolate); + } + if (flags & LU_HAS_ROTZ) { + LocalInterpComp_Angle(&prev->rotZ, &next->rotZ, update->rotZ, interpolate); + } + if (flags & LU_HAS_YAW) { + LocalInterpComp_Angle(&prev->yaw, &next->yaw, update->yaw, interpolate); + + if (!interpolate) { + next->rotY = next->yaw; + interp->RotYCount = 0; + } else { + /* Body Y rotation lags slightly behind */ + InterpComp_AddRotY(interp, Math_LerpAngle(prev->yaw, next->yaw, 0.33333333f)); + InterpComp_AddRotY(interp, Math_LerpAngle(prev->yaw, next->yaw, 0.66666667f)); + InterpComp_AddRotY(interp, Math_LerpAngle(prev->yaw, next->yaw, 1.00000000f)); + + e->next.rotY = interp->RotYStates[0]; + } + } + Entity_LerpAngles(e, 0.0f); +} + +void LocalInterpComp_AdvanceState(struct InterpComp* interp, struct Entity* e) { + e->prev = e->next; + e->Position = e->prev.pos; + InterpComp_AdvanceRotY(interp, e); +} + + +/*########################################################################################################################* +*---------------------------------------------------CollisionsComponent---------------------------------------------------* +*#########################################################################################################################*/ +/* Whether a collision occurred with any horizontal sides of any blocks */ +cc_bool Collisions_HitHorizontal(struct CollisionsComp* comp) { + return comp->HitXMin || comp->HitXMax || comp->HitZMin || comp->HitZMax; +} +#define COLLISIONS_ADJ 0.001f + +static void Collisions_ClipX(struct Entity* e, Vec3* size, struct AABB* entityBB, struct AABB* extentBB) { + e->Velocity.x = 0.0f; + entityBB->Min.x = e->Position.x - size->x / 2; extentBB->Min.x = entityBB->Min.x; + entityBB->Max.x = e->Position.x + size->x / 2; extentBB->Max.x = entityBB->Max.x; +} + +static void Collisions_ClipY(struct Entity* e, Vec3* size, struct AABB* entityBB, struct AABB* extentBB) { + e->Velocity.y = 0.0f; + entityBB->Min.y = e->Position.y; extentBB->Min.y = entityBB->Min.y; + entityBB->Max.y = e->Position.y + size->y; extentBB->Max.y = entityBB->Max.y; +} + +static void Collisions_ClipZ(struct Entity* e, Vec3* size, struct AABB* entityBB, struct AABB* extentBB) { + e->Velocity.z = 0.0f; + entityBB->Min.z = e->Position.z - size->z / 2; extentBB->Min.z = entityBB->Min.z; + entityBB->Max.z = e->Position.z + size->z / 2; extentBB->Max.z = entityBB->Max.z; +} + +static cc_bool Collisions_CanSlideThrough(struct AABB* adjFinalBB) { + IVec3 bbMin, bbMax; + struct AABB blockBB; + BlockID block; + Vec3 v; + int x, y, z; + + IVec3_Floor(&bbMin, &adjFinalBB->Min); + IVec3_Floor(&bbMax, &adjFinalBB->Max); + + for (y = bbMin.y; y <= bbMax.y; y++) { v.y = (float)y; + for (z = bbMin.z; z <= bbMax.z; z++) { v.z = (float)z; + for (x = bbMin.x; x <= bbMax.x; x++) { v.x = (float)x; + + block = World_GetPhysicsBlock(x, y, z); + Vec3_Add(&blockBB.Min, &v, &Blocks.MinBB[block]); + Vec3_Add(&blockBB.Max, &v, &Blocks.MaxBB[block]); + + if (!AABB_Intersects(&blockBB, adjFinalBB)) continue; + if (Blocks.Collide[block] == COLLIDE_SOLID) return false; + } + } + } + return true; +} + +static cc_bool Collisions_DidSlide(struct CollisionsComp* comp, struct AABB* blockBB, Vec3* size, + struct AABB* finalBB, struct AABB* entityBB, struct AABB* extentBB) { + float yDist = blockBB->Max.y - entityBB->Min.y; + struct AABB adjBB; + + if (yDist > 0.0f && yDist <= comp->StepSize + 0.01f) { + float blockBB_MinX = max(blockBB->Min.x, blockBB->Max.x - size->x / 2); + float blockBB_MaxX = min(blockBB->Max.x, blockBB->Min.x + size->x / 2); + float blockBB_MinZ = max(blockBB->Min.z, blockBB->Max.z - size->z / 2); + float blockBB_MaxZ = min(blockBB->Max.z, blockBB->Min.z + size->z / 2); + + adjBB.Min.x = min(finalBB->Min.x, blockBB_MinX + COLLISIONS_ADJ); + adjBB.Max.x = max(finalBB->Max.x, blockBB_MaxX - COLLISIONS_ADJ); + adjBB.Min.y = blockBB->Max.y + COLLISIONS_ADJ; + adjBB.Max.y = adjBB.Min.y + size->y; + adjBB.Min.z = min(finalBB->Min.z, blockBB_MinZ + COLLISIONS_ADJ); + adjBB.Max.z = max(finalBB->Max.z, blockBB_MaxZ - COLLISIONS_ADJ); + + if (!Collisions_CanSlideThrough(&adjBB)) return false; + comp->Entity->Position.y = adjBB.Min.y; + comp->Entity->OnGround = true; + Collisions_ClipY(comp->Entity, size, entityBB, extentBB); + return true; + } + return false; +} + +static void Collisions_ClipXMin(struct CollisionsComp* comp, struct AABB* blockBB, struct AABB* entityBB, + cc_bool wasOn, struct AABB* finalBB, struct AABB* extentBB, Vec3* size) { + if (!wasOn || !Collisions_DidSlide(comp, blockBB, size, finalBB, entityBB, extentBB)) { + comp->Entity->Position.x = blockBB->Min.x - size->x / 2 - COLLISIONS_ADJ; + Collisions_ClipX(comp->Entity, size, entityBB, extentBB); + comp->HitXMin = true; + } +} + +static void Collisions_ClipXMax(struct CollisionsComp* comp, struct AABB* blockBB, struct AABB* entityBB, + cc_bool wasOn, struct AABB* finalBB, struct AABB* extentBB, Vec3* size) { + if (!wasOn || !Collisions_DidSlide(comp, blockBB, size, finalBB, entityBB, extentBB)) { + comp->Entity->Position.x = blockBB->Max.x + size->x / 2 + COLLISIONS_ADJ; + Collisions_ClipX(comp->Entity, size, entityBB, extentBB); + comp->HitXMax = true; + } +} + +static void Collisions_ClipZMax(struct CollisionsComp* comp, struct AABB* blockBB, struct AABB* entityBB, + cc_bool wasOn, struct AABB* finalBB, struct AABB* extentBB, Vec3* size) { + if (!wasOn || !Collisions_DidSlide(comp, blockBB, size, finalBB, entityBB, extentBB)) { + comp->Entity->Position.z = blockBB->Max.z + size->z / 2 + COLLISIONS_ADJ; + Collisions_ClipZ(comp->Entity, size, entityBB, extentBB); + comp->HitZMax = true; + } +} + +static void Collisions_ClipZMin(struct CollisionsComp* comp, struct AABB* blockBB, struct AABB* entityBB, + cc_bool wasOn, struct AABB* finalBB, struct AABB* extentBB, Vec3* size) { + if (!wasOn || !Collisions_DidSlide(comp, blockBB, size, finalBB, entityBB, extentBB)) { + comp->Entity->Position.z = blockBB->Min.z - size->z / 2 - COLLISIONS_ADJ; + Collisions_ClipZ(comp->Entity, size, entityBB, extentBB); + comp->HitZMin = true; + } +} + +static void Collisions_ClipYMin(struct CollisionsComp* comp, struct AABB* blockBB, struct AABB* entityBB, + struct AABB* extentBB, Vec3* size) { + comp->Entity->Position.y = blockBB->Min.y - size->y - COLLISIONS_ADJ; + Collisions_ClipY(comp->Entity, size, entityBB, extentBB); + comp->HitYMin = true; +} + +static void Collisions_ClipYMax(struct CollisionsComp* comp, struct AABB* blockBB, struct AABB* entityBB, + struct AABB* extentBB, Vec3* size) { + comp->Entity->Position.y = blockBB->Max.y + COLLISIONS_ADJ; + comp->Entity->OnGround = true; + Collisions_ClipY(comp->Entity, size, entityBB, extentBB); + comp->HitYMax = true; +} + +static void Collisions_CollideWithReachableBlocks(struct CollisionsComp* comp, int count, struct AABB* entityBB, + struct AABB* extentBB) { + struct Entity* entity = comp->Entity; + struct SearcherState state; + struct AABB blockBB, finalBB; + Vec3 size; + cc_bool wasOn; + + Vec3 bPos, v; + float tx, ty, tz; + int i, block; + + /* Reset collision detection states */ + wasOn = entity->OnGround; + entity->OnGround = false; + comp->HitXMin = false; comp->HitYMin = false; comp->HitZMin = false; + comp->HitXMax = false; comp->HitYMax = false; comp->HitZMax = false; + + size = entity->Size; + for (i = 0; i < count; i++) { + /* Unpack the block and coordinate data */ + state = Searcher_States[i]; + bPos.x = state.x >> 3; bPos.y = state.y >> 4; bPos.z = state.z >> 3; + block = (state.x & 0x7) | (state.y & 0xF) << 3 | (state.z & 0x7) << 7; + + Vec3_Add(&blockBB.Min, &Blocks.MinBB[block], &bPos); + Vec3_Add(&blockBB.Max, &Blocks.MaxBB[block], &bPos); + if (!AABB_Intersects(extentBB, &blockBB)) continue; + + /* Recheck time to collide with block (as colliding with blocks modifies this) */ + Searcher_CalcTime(&entity->Velocity, entityBB, &blockBB, &tx, &ty, &tz); + if (tx > 1.0f || ty > 1.0f || tz > 1.0f) { + Platform_LogConst("t > 1 in physics calculation.. this shouldn't have happened."); + } + + /* Calculate the location of the entity when it collides with this block */ + v = entity->Velocity; + v.x *= tx; v.y *= ty; v.z *= tz; + /* Inlined ABBB_Offset */ + Vec3_Add(&finalBB.Min, &entityBB->Min, &v); + Vec3_Add(&finalBB.Max, &entityBB->Max, &v); + + /* if we have hit the bottom of a block, we need to change the axis we test first */ + if (!comp->HitYMin) { + if (finalBB.Min.y + COLLISIONS_ADJ >= blockBB.Max.y) { + Collisions_ClipYMax(comp, &blockBB, entityBB, extentBB, &size); + } else if (finalBB.Max.y - COLLISIONS_ADJ <= blockBB.Min.y) { + Collisions_ClipYMin(comp, &blockBB, entityBB, extentBB, &size); + } else if (finalBB.Min.x + COLLISIONS_ADJ >= blockBB.Max.x) { + Collisions_ClipXMax(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Max.x - COLLISIONS_ADJ <= blockBB.Min.x) { + Collisions_ClipXMin(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Min.z + COLLISIONS_ADJ >= blockBB.Max.z) { + Collisions_ClipZMax(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Max.z - COLLISIONS_ADJ <= blockBB.Min.z) { + Collisions_ClipZMin(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } + continue; + } + + /* if flying or falling, test the horizontal axes first */ + if (finalBB.Min.x + COLLISIONS_ADJ >= blockBB.Max.x) { + Collisions_ClipXMax(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Max.x - COLLISIONS_ADJ <= blockBB.Min.x) { + Collisions_ClipXMin(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Min.z + COLLISIONS_ADJ >= blockBB.Max.z) { + Collisions_ClipZMax(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Max.z - COLLISIONS_ADJ <= blockBB.Min.z) { + Collisions_ClipZMin(comp, &blockBB, entityBB, wasOn, &finalBB, extentBB, &size); + } else if (finalBB.Min.y + COLLISIONS_ADJ >= blockBB.Max.y) { + Collisions_ClipYMax(comp, &blockBB, entityBB, extentBB, &size); + } else if (finalBB.Max.y - COLLISIONS_ADJ <= blockBB.Min.y) { + Collisions_ClipYMin(comp, &blockBB, entityBB, extentBB, &size); + } + } +} + +/* TODO: test for corner cases, and refactor this */ +void Collisions_MoveAndWallSlide(struct CollisionsComp* comp) { + struct Entity* e = comp->Entity; + struct AABB entityBB, entityExtentBB; + int count; + + if (Vec3_IsZero(e->Velocity)) return; + count = Searcher_FindReachableBlocks(e, &entityBB, &entityExtentBB); + Collisions_CollideWithReachableBlocks(comp, count, &entityBB, &entityExtentBB); +} + + +/*########################################################################################################################* +*----------------------------------------------------PhysicsComponent-----------------------------------------------------* +*#########################################################################################################################*/ +void PhysicsComp_Init(struct PhysicsComp* comp, struct Entity* entity) { + Mem_Set(comp, 0, sizeof(struct PhysicsComp)); + comp->CanLiquidJump = true; + comp->Entity = entity; + comp->JumpVel = 0.42f; + comp->UserJumpVel = 0.42f; + comp->ServerJumpVel = 0.42f; +} + +static cc_bool PhysicsComp_TouchesLiquid(BlockID block) { return Blocks.Collide[block] == COLLIDE_LIQUID; } +void PhysicsComp_UpdateVelocityState(struct PhysicsComp* comp) { + struct Entity* entity = comp->Entity; + struct HacksComp* hacks = comp->Hacks; + struct AABB bounds; + int dir; + + cc_bool touchWater, touchLava; + cc_bool liquidFeet, liquidRest; + int feetY, bodyY, headY; + cc_bool pastJumpPoint; + + if (hacks->Floating) { + entity->Velocity.y = 0.0f; /* eliminate the effect of gravity */ + dir = (hacks->FlyingUp || comp->Jumping) ? 1 : (hacks->FlyingDown ? -1 : 0); + + entity->Velocity.y += 0.12f * dir; + if (hacks->Speeding && hacks->CanSpeed) entity->Velocity.y += 0.12f * dir; + if (hacks->HalfSpeeding && hacks->CanSpeed) entity->Velocity.y += 0.06f * dir; + } else if (comp->Jumping && Entity_TouchesAnyRope(entity) && entity->Velocity.y > 0.02f) { + entity->Velocity.y = 0.02f; + } + + if (!comp->Jumping) { comp->CanLiquidJump = false; return; } + touchWater = Entity_TouchesAnyWater(entity); + touchLava = Entity_TouchesAnyLava(entity); + + if (touchWater || touchLava) { + Entity_GetBounds(entity, &bounds); + feetY = Math_Floor(bounds.Min.y); bodyY = feetY + 1; + headY = Math_Floor(bounds.Max.y); + if (bodyY > headY) bodyY = headY; + + bounds.Max.y = bounds.Min.y = feetY; + liquidFeet = Entity_TouchesAny(&bounds, PhysicsComp_TouchesLiquid); + bounds.Min.y = min(bodyY, headY); + bounds.Max.y = max(bodyY, headY); + liquidRest = Entity_TouchesAny(&bounds, PhysicsComp_TouchesLiquid); + + pastJumpPoint = liquidFeet && !liquidRest && (Math_Mod1(entity->Position.y) >= 0.4f); + if (!pastJumpPoint) { + comp->CanLiquidJump = true; + entity->Velocity.y += 0.04f; + if (hacks->Speeding && hacks->CanSpeed) entity->Velocity.y += 0.04f; + if (hacks->HalfSpeeding && hacks->CanSpeed) entity->Velocity.y += 0.02f; + } else if (pastJumpPoint) { + /* either A) climb up solid on side B) jump bob in water */ + if (Collisions_HitHorizontal(comp->Collisions)) { + entity->Velocity.y += touchLava ? 0.30f : 0.13f; + } else if (comp->CanLiquidJump) { + entity->Velocity.y += touchLava ? 0.20f : 0.10f; + } + comp->CanLiquidJump = false; + } + } else if (comp->UseLiquidGravity) { + entity->Velocity.y += 0.04f; + if (hacks->Speeding && hacks->CanSpeed) entity->Velocity.y += 0.04f; + if (hacks->HalfSpeeding && hacks->CanSpeed) entity->Velocity.y += 0.02f; + comp->CanLiquidJump = false; + } else if (Entity_TouchesAnyRope(entity)) { + entity->Velocity.y += (hacks->Speeding && hacks->CanSpeed) ? 0.15f : 0.10f; + comp->CanLiquidJump = false; + } else if (entity->OnGround) { + PhysicsComp_DoNormalJump(comp); + } +} + +void PhysicsComp_DoNormalJump(struct PhysicsComp* comp) { + struct Entity* entity = comp->Entity; + struct HacksComp* hacks = comp->Hacks; + if (comp->JumpVel == 0.0f || hacks->MaxJumps <= 0) return; + + entity->Velocity.y = comp->JumpVel; + if (hacks->Speeding && hacks->CanSpeed) entity->Velocity.y += comp->JumpVel; + if (hacks->HalfSpeeding && hacks->CanSpeed) entity->Velocity.y += comp->JumpVel / 2; + comp->CanLiquidJump = false; +} + +static cc_bool PhysicsComp_TouchesSlipperyIce(BlockID b) { return Blocks.ExtendedCollide[b] == COLLIDE_SLIPPERY_ICE; } +static cc_bool PhysicsComp_OnIce(struct Entity* e) { + struct AABB bounds; + int feetX, feetY, feetZ; + BlockID feetBlock; + + feetX = Math_Floor(e->Position.x); + feetY = Math_Floor(e->Position.y - 0.01f); + feetZ = Math_Floor(e->Position.z); + + feetBlock = World_GetPhysicsBlock(feetX, feetY, feetZ); + if (Blocks.ExtendedCollide[feetBlock] == COLLIDE_ICE) return true; + + Entity_GetBounds(e, &bounds); + bounds.Min.y -= 0.01f; bounds.Max.y = bounds.Min.y; + return Entity_TouchesAny(&bounds, PhysicsComp_TouchesSlipperyIce); +} + +static void PhysicsComp_MoveHor(struct PhysicsComp* comp, Vec3 vel, float factor) { + struct Entity* entity; + float dist; + + dist = Math_SqrtF(vel.x * vel.x + vel.z * vel.z); + if (dist < 0.00001f) return; + if (dist < 1.0f) dist = 1.0f; + + /* entity.Velocity += vel * (factor / dist) */ + entity = comp->Entity; + Vec3_Mul1By(&vel, factor / dist); + Vec3_AddBy(&entity->Velocity, &vel); +} + +static void PhysicsComp_Move(struct PhysicsComp* comp, Vec3 drag, float gravity, float yMul) { + struct Entity* entity = comp->Entity; + entity->Velocity.y *= yMul; + + if (!comp->Hacks->Noclip) { + Collisions_MoveAndWallSlide(comp->Collisions); + } + Vec3_AddBy(&entity->Position, &entity->Velocity); + + entity->Velocity.y /= yMul; + Vec3_Mul3By(&entity->Velocity, &drag); + entity->Velocity.y -= gravity; +} + +static void PhysicsComp_MoveFlying(struct PhysicsComp* comp, Vec3 vel, float factor, Vec3 drag, float gravity, float yMul) { + struct Entity* entity = comp->Entity; + struct HacksComp* hacks = comp->Hacks; + float yVel; + + PhysicsComp_MoveHor(comp, vel, factor); + yVel = Math_SqrtF(entity->Velocity.x * entity->Velocity.x + entity->Velocity.z * entity->Velocity.z); + /* make horizontal speed the same as vertical speed */ + if ((vel.x != 0.0f || vel.z != 0.0f) && yVel > 0.001f) { + entity->Velocity.y = 0.0f; + yMul = 1.0f; + if (hacks->FlyingUp || comp->Jumping) entity->Velocity.y += yVel; + if (hacks->FlyingDown) entity->Velocity.y -= yVel; + } + PhysicsComp_Move(comp, drag, gravity, yMul); +} + +static void PhysicsComp_MoveNormal(struct PhysicsComp* comp, Vec3 vel, float factor, Vec3 drag, float gravity, float yMul) { + PhysicsComp_MoveHor(comp, vel, factor); + PhysicsComp_Move(comp, drag, gravity, yMul); +} + +static float PhysicsComp_LowestModifier(struct PhysicsComp* comp, struct AABB* bounds, cc_bool checkSolid) { + IVec3 bbMin, bbMax; + float modifier = MATH_LARGENUM; + struct AABB blockBB; + BlockID block; + cc_uint8 collide; + Vec3 v; + int x, y, z; + + IVec3_Floor(&bbMin, &bounds->Min); + IVec3_Floor(&bbMax, &bounds->Max); + + bbMin.x = max(bbMin.x, 0); bbMax.x = min(bbMax.x, World.MaxX); + bbMin.y = max(bbMin.y, 0); bbMax.y = min(bbMax.y, World.MaxY); + bbMin.z = max(bbMin.z, 0); bbMax.z = min(bbMax.z, World.MaxZ); + + for (y = bbMin.y; y <= bbMax.y; y++) { v.y = (float)y; + for (z = bbMin.z; z <= bbMax.z; z++) { v.z = (float)z; + for (x = bbMin.x; x <= bbMax.x; x++) { v.x = (float)x; + block = World_GetBlock(x, y, z); + + if (block == BLOCK_AIR) continue; + collide = Blocks.Collide[block]; + if (collide == COLLIDE_SOLID && !checkSolid) continue; + + Vec3_Add(&blockBB.Min, &v, &Blocks.MinBB[block]); + Vec3_Add(&blockBB.Max, &v, &Blocks.MaxBB[block]); + if (!AABB_Intersects(&blockBB, bounds)) continue; + + modifier = min(modifier, Blocks.SpeedMultiplier[block]); + if (Blocks.ExtendedCollide[block] == COLLIDE_LIQUID) { + comp->UseLiquidGravity = true; + } + } + } + } + return modifier; +} + +static float PhysicsComp_GetSpeed(struct HacksComp* hacks, float speedMul, cc_bool canSpeed) { + float factor = hacks->Floating ? speedMul : 1.0f; + float speed = factor * (1 + HacksComp_CalcSpeedFactor(hacks, canSpeed)); + return hacks->CanSpeed ? speed : min(speed, hacks->MaxHorSpeed); +} + +static float PhysicsComp_GetBaseSpeed(struct PhysicsComp* comp) { + struct AABB bounds; + float baseModifier, solidModifier; + + Entity_GetBounds(comp->Entity, &bounds); + comp->UseLiquidGravity = false; + + baseModifier = PhysicsComp_LowestModifier(comp, &bounds, false); + bounds.Min.y -= 0.5f/16.0f; /* also check block standing on */ + solidModifier = PhysicsComp_LowestModifier(comp, &bounds, true); + + if (baseModifier == MATH_LARGENUM && solidModifier == MATH_LARGENUM) return 1.0f; + return baseModifier == MATH_LARGENUM ? solidModifier : baseModifier; +} + +#define LIQUID_GRAVITY 0.02f +#define ROPE_GRAVITY 0.034f +void PhysicsComp_PhysicsTick(struct PhysicsComp* comp, Vec3 vel) { + struct Entity* entity = comp->Entity; + struct HacksComp* hacks = comp->Hacks; + float baseSpeed, verSpeed, horSpeed; + float factor, gravity; + cc_bool womSpeedBoost; + + if (hacks->Noclip) entity->OnGround = false; + baseSpeed = PhysicsComp_GetBaseSpeed(comp); + verSpeed = baseSpeed * (PhysicsComp_GetSpeed(hacks, 8.0f, hacks->CanSpeed) / 5.0f); + horSpeed = baseSpeed * PhysicsComp_GetSpeed(hacks, 8.0f / 5.0f, true) * hacks->BaseHorSpeed; + /* previously horSpeed used to be multiplied by factor of 0.02 in last case */ + /* it's now multiplied by 0.1, so need to divide by 5 so user speed modifier comes out same */ + + /* TODO: this is a temp fix to avoid crashing for high horizontal speed */ + Math_Clamp(horSpeed, -75.0f, 75.0f); + /* vertical speed never goes below: base speed * 1.0 */ + if (verSpeed < baseSpeed) verSpeed = baseSpeed; + + womSpeedBoost = hacks->CanDoubleJump && hacks->WOMStyleHacks; + if (!hacks->Floating && womSpeedBoost) { + if (comp->MultiJumps == 1) { horSpeed *= 46.5f; verSpeed *= 7.5f; } + else if (comp->MultiJumps > 1) { horSpeed *= 93.0f; verSpeed *= 10.0f; } + } + + if (Entity_TouchesAnyWater(entity) && !hacks->Floating) { + Vec3 waterDrag = { 0.8f, 0.8f, 0.8f }; + PhysicsComp_MoveNormal(comp, vel, 0.02f * horSpeed, waterDrag, LIQUID_GRAVITY, verSpeed); + } else if (Entity_TouchesAnyLava(entity) && !hacks->Floating) { + Vec3 lavaDrag = { 0.5f, 0.5f, 0.5f }; + PhysicsComp_MoveNormal(comp, vel, 0.02f * horSpeed, lavaDrag, LIQUID_GRAVITY, verSpeed); + } else if (Entity_TouchesAnyRope(entity) && !hacks->Floating) { + Vec3 ropeDrag = { 0.5f, 0.85f, 0.5f }; + PhysicsComp_MoveNormal(comp, vel, 0.02f * 1.7f, ropeDrag, ROPE_GRAVITY, verSpeed); + } else { + factor = hacks->Floating || entity->OnGround ? 0.1f : 0.02f; + gravity = comp->UseLiquidGravity ? LIQUID_GRAVITY : entity->Model->gravity; + + if (hacks->Floating) { + PhysicsComp_MoveFlying(comp, vel, factor * horSpeed, entity->Model->drag, gravity, verSpeed); + } else { + PhysicsComp_MoveNormal(comp, vel, factor * horSpeed, entity->Model->drag, gravity, verSpeed); + } + + if (PhysicsComp_OnIce(entity) && !hacks->Floating) { + /* limit components to +-0.25f by rescaling vector to [-0.25, 0.25] */ + if (Math_AbsF(entity->Velocity.x) > 0.25f || Math_AbsF(entity->Velocity.z) > 0.25f) { + float xScale = Math_AbsF(0.25f / entity->Velocity.x); + float zScale = Math_AbsF(0.25f / entity->Velocity.z); + + float scale = min(xScale, zScale); + entity->Velocity.x *= scale; + entity->Velocity.z *= scale; + } + } else if (entity->OnGround || hacks->Flying) { + Vec3_Mul3By(&entity->Velocity, &entity->Model->groundFriction); /* air drag or ground friction */ + } + } + + if (entity->OnGround) comp->MultiJumps = 0; +} + +static double PhysicsComp_YPosAt(int t, float u) { + /* v(t, u) = (4 + u) * (0.98^t) - 4, where u = initial velocity */ + /* x(t, u) = Σv(t, u) from 0 to t (since we work in discrete timesteps) */ + /* plugging into Wolfram Alpha gives 1 equation as */ + /* (0.98^t) * (-49u - 196) - 4t + 50u + 196 */ + double a = Math_Exp2(-0.02914633510256746 * t); /* ~0.98^t */ + return a * (-49 * u - 196) - 4 * t + 50 * u + 196; +} + +double PhysicsComp_CalcMaxHeight(float u) { + /* equation below comes from solving diff(x(t, u))= 0 */ + /* We only work in discrete timesteps, so test both rounded up and down */ + double t = 34.30961849 * Math_Log2(0.247483075 * u + 0.9899323); + double value_floor = PhysicsComp_YPosAt((int)t, u); + double value_ceil = PhysicsComp_YPosAt((int)t + 1, u); + return max(value_floor, value_ceil); +} + +/* Calculates the jump velocity required such that when user presses +the jump binding they will be able to jump up to the given height. */ +float PhysicsComp_CalcJumpVelocity(float jumpHeight) { + float jumpVel = 0.0f; + if (jumpHeight == 0.0f) return jumpVel; + + if (jumpHeight >= 256.0f) jumpVel = 10.0f; + if (jumpHeight >= 512.0f) jumpVel = 16.5f; + if (jumpHeight >= 768.0f) jumpVel = 22.5f; + + while (PhysicsComp_CalcMaxHeight(jumpVel) <= jumpHeight) { jumpVel += 0.001f; } + return jumpVel; +} + +void PhysicsComp_DoEntityPush(struct Entity* entity) { + struct Entity* other; + cc_bool yIntersects; + Vec3 dir; + float dist, pushStrength; + int id; + dir.y = 0.0f; + + for (id = 0; id < ENTITIES_MAX_COUNT; id++) { + other = Entities.List[id]; + if (!other || other == entity) continue; + if (!other->Model->pushes) continue; + + yIntersects = + entity->Position.y <= (other->Position.y + other->Size.y) && + other->Position.y <= (entity->Position.y + entity->Size.y); + if (!yIntersects) continue; + + dir.x = other->Position.x - entity->Position.x; + dir.z = other->Position.z - entity->Position.z; + dist = dir.x * dir.x + dir.z * dir.z; + if (dist < 0.002f || dist > 1.0f) continue; /* TODO: range needs to be lower? */ + + Vec3_Normalise(&dir); + pushStrength = (1 - dist) / 32.0f; /* TODO: should be 24/25 */ + /* entity.Velocity -= dir * pushStrength */ + Vec3_Mul1By(&dir, pushStrength); + Vec3_SubBy(&entity->Velocity, &dir); + } +} + + +/*########################################################################################################################* +*----------------------------------------------------SoundsComponent------------------------------------------------------* +*#########################################################################################################################*/ +static Vec3 sounds_lastPos = { -87.1234f, -99.5678f, -100.91237f }; +static cc_bool sounds_anyNonAir; +static cc_uint8 sounds_type; + +static cc_bool Sounds_CheckNonSolid(BlockID b) { + cc_uint8 type = Blocks.StepSounds[b]; + cc_uint8 collide = Blocks.Collide[b]; + if (type != SOUND_NONE && collide != COLLIDE_SOLID) sounds_type = type; + + if (Blocks.Draw[b] != DRAW_GAS) sounds_anyNonAir = true; + return false; +} + +static cc_bool Sounds_CheckSolid(BlockID b) { + cc_uint8 type = Blocks.StepSounds[b]; + if (type != SOUND_NONE) sounds_type = type; + + if (Blocks.Draw[b] != DRAW_GAS) sounds_anyNonAir = true; + return false; +} + +static void SoundComp_GetSound(struct LocalPlayer* p) { + struct AABB bounds; + Vec3 pos; + IVec3 coords; + BlockID blockUnder; + float maxY; + cc_uint8 typeUnder, collideUnder; + + Entity_GetBounds(&p->Base, &bounds); + sounds_type = SOUND_NONE; + sounds_anyNonAir = false; + + /* first check surrounding liquids/gas for sounds */ + Entity_TouchesAny(&bounds, Sounds_CheckNonSolid); + if (sounds_type != SOUND_NONE) return; + + /* then check block standing on (feet) */ + pos = p->Base.next.pos; pos.y -= 0.01f; + IVec3_Floor(&coords, &pos); + blockUnder = World_SafeGetBlock(coords.x, coords.y, coords.z); + maxY = coords.y + Blocks.MaxBB[blockUnder].y; + + typeUnder = Blocks.StepSounds[blockUnder]; + collideUnder = Blocks.Collide[blockUnder]; + if (maxY >= pos.y && collideUnder == COLLIDE_SOLID && typeUnder != SOUND_NONE) { + sounds_anyNonAir = true; sounds_type = typeUnder; return; + } + + /* then check all solid blocks at feet */ + bounds.Max.y = bounds.Min.y = pos.y; + Entity_TouchesAny(&bounds, Sounds_CheckSolid); +} + +static cc_bool SoundComp_ShouldPlay(struct LocalPlayer* p, Vec3 soundPos) { + Vec3 delta; + float distSq; + float oldLegRot, newLegRot; + + Vec3_Sub(&delta, &sounds_lastPos, &soundPos); + distSq = Vec3_LengthSquared(&delta); + /* just play every certain block interval when not animating */ + if (p->Base.Anim.Swing < 0.999f) return distSq > 1.75f * 1.75f; + + /* have our legs just crossed over the '0' point? */ + if (Camera.Active->isThirdPerson) { + oldLegRot = Math_CosF(p->Base.Anim.WalkTimeO); + newLegRot = Math_CosF(p->Base.Anim.WalkTimeN); + } else { + oldLegRot = Math_SinF(p->Base.Anim.WalkTimeO); + newLegRot = Math_SinF(p->Base.Anim.WalkTimeN); + } + return Math_Sign(oldLegRot) != Math_Sign(newLegRot); +} + +void SoundComp_Tick(struct LocalPlayer* p, cc_bool wasOnGround) { + Vec3 soundPos = p->Base.next.pos; + + SoundComp_GetSound(p); + if (!sounds_anyNonAir) soundPos = Vec3_BigPos(); + + if (p->Base.OnGround && (SoundComp_ShouldPlay(p, soundPos) || !wasOnGround)) { + Audio_PlayStepSound(sounds_type); + sounds_lastPos = soundPos; + } +} diff --git a/src/EntityComponents.h b/src/EntityComponents.h new file mode 100644 index 0000000..30f2882 --- /dev/null +++ b/src/EntityComponents.h @@ -0,0 +1,135 @@ +#ifndef CC_ENTITY_COMPONENTS_H +#define CC_ENTITY_COMPONENTS_H +#include "Vectors.h" +#include "Constants.h" +/* Various components for entities. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct Entity; +struct LocationUpdate; +struct LocalPlayer; + +/* Entity component that performs model animation depending on movement speed and time */ +struct AnimatedComp { + float BobbingHor, BobbingVer, BobbingModel; + float WalkTime, Swing, BobStrength; + float WalkTimeO, WalkTimeN, SwingO, SwingN, BobStrengthO, BobStrengthN; + + float LeftLegX, LeftLegZ, RightLegX, RightLegZ; + float LeftArmX, LeftArmZ, RightArmX, RightArmZ; +}; + +void AnimatedComp_Init(struct AnimatedComp* anim); +void AnimatedComp_Update(struct Entity* entity, Vec3 oldPos, Vec3 newPos, float delta); +void AnimatedComp_GetCurrent(struct Entity* entity, float t); + +/* Entity component that performs tilt animation depending on movement speed and time */ +struct TiltComp { + float TiltX, TiltY, VelTiltStrength; + float VelTiltStrengthO, VelTiltStrengthN; +}; + +void TiltComp_Init(struct TiltComp* anim); +void TiltComp_Update(struct LocalPlayer* p, struct TiltComp* anim, float delta); +void TiltComp_GetCurrent(struct LocalPlayer* p, struct TiltComp* anim, float t); + +/* Entity component that performs management of hack states */ +struct HacksComp { + cc_bool IsOp; + cc_bool Floating; /* true if NoClip or Flying */ + /* Speed player move at, relative to normal speed, when the 'speeding' input binding is active */ + float SpeedMultiplier; + /* Whether blocks that the player places that intersect themselves, should cause the player to + be pushed back in the opposite direction of the placed block */ + cc_bool PushbackPlacing; + /* Whether the player should be able to step up whole blocks, instead of just slabs */ + cc_bool FullBlockStep; + /* Whether user has allowed hacks as an option. Note 'can use X' set by the server override this */ + cc_bool Enabled; + + cc_bool CanAnyHacks, CanUseThirdPerson, CanSpeed, CanFly; + cc_bool CanRespawn, CanNoclip, CanPushbackBlocks,CanSeeAllNames; + cc_bool CanDoubleJump, CanBePushed; + float BaseHorSpeed; + /* Max amount of jumps the player can perform */ + int MaxJumps; + + /* Whether the player should slide after letting go of movement buttons in noclip */ + cc_bool NoclipSlide; + /* Whether the player has allowed the usage of fast double jumping abilities */ + cc_bool WOMStyleHacks; + + cc_bool Noclip, Flying, FlyingUp, FlyingDown, Speeding, HalfSpeeding; + float MaxHorSpeed; + cc_string HacksFlags; + char __HacksFlagsBuffer[STRING_SIZE * 2]; +}; + +void HacksComp_Init(struct HacksComp* hacks); +cc_bool HacksComp_CanJumpHigher(struct HacksComp* hacks); +/* Determines hacks permissions based on flags, then calls HacksComp_Update */ +/* e.g. +ophax allows all hacks if op, -push disables entity pushing */ +void HacksComp_RecheckFlags(struct HacksComp* hacks); +/* Updates state based on permissions (e.g. Flying set to false if CanFly is false) */ +/* Raises UserEvents.HackPermsChanged */ +void HacksComp_Update(struct HacksComp* hacks); +void HacksComp_SetFlying(struct HacksComp* hacks, cc_bool flying); +void HacksComp_SetNoclip(struct HacksComp* hacks, cc_bool noclip); +float HacksComp_CalcSpeedFactor(struct HacksComp* hacks, cc_bool canSpeed); + +#define InterpComp_Layout int RotYCount; float RotYStates[15]; +/* Base entity component that performs interpolation of position and orientation */ +struct InterpComp { InterpComp_Layout }; + +void LocalInterpComp_SetLocation(struct InterpComp* interp, struct LocationUpdate* update, struct Entity* e); +void LocalInterpComp_AdvanceState(struct InterpComp* interp, struct Entity* e); + +/* Represents a network orientation state */ +struct NetInterpAngles { float Pitch, Yaw, RotX, RotZ; }; + +/* Entity component that performs interpolation for network players */ +struct NetInterpComp { + InterpComp_Layout + /* Last known position and orientation sent by the server */ + Vec3 CurPos; struct NetInterpAngles CurAngles; + /* Interpolated position and orientation state */ + int PositionsCount, AnglesCount; + Vec3 Positions[10]; struct NetInterpAngles Angles[10]; +}; + +void NetInterpComp_SetLocation(struct NetInterpComp* interp, struct LocationUpdate* update, struct Entity* e); +void NetInterpComp_AdvanceState(struct NetInterpComp* interp, struct Entity* e); + +/* Entity component that performs collision detection */ +struct CollisionsComp { + struct Entity* Entity; + cc_bool HitXMin, HitYMin, HitZMin, HitXMax, HitYMax, HitZMax, WasOn; + float StepSize; +}; +cc_bool Collisions_HitHorizontal(struct CollisionsComp* comp); +void Collisions_MoveAndWallSlide(struct CollisionsComp* comp); + +/* Entity component that performs collisions */ +struct PhysicsComp { + cc_bool UseLiquidGravity; /* used by BlockDefinitions */ + cc_bool CanLiquidJump, Jumping; + int MultiJumps; + struct Entity* Entity; + + float JumpVel, UserJumpVel, ServerJumpVel; + struct HacksComp* Hacks; + struct CollisionsComp* Collisions; +}; + +void PhysicsComp_Init(struct PhysicsComp* comp, struct Entity* entity); +void PhysicsComp_UpdateVelocityState(struct PhysicsComp* comp); +void PhysicsComp_DoNormalJump(struct PhysicsComp* comp); +void PhysicsComp_PhysicsTick(struct PhysicsComp* comp, Vec3 vel); +float PhysicsComp_CalcJumpVelocity(float jumpHeight); +double PhysicsComp_CalcMaxHeight(float u); +void PhysicsComp_DoEntityPush(struct Entity* entity); + +/* Entity component that plays block step sounds */ +void SoundComp_Tick(struct LocalPlayer* p, cc_bool wasOnGround); +#endif diff --git a/src/EntityRenderers.c b/src/EntityRenderers.c new file mode 100644 index 0000000..e45d681 --- /dev/null +++ b/src/EntityRenderers.c @@ -0,0 +1,478 @@ +#include "EntityRenderers.h" +#include "Entity.h" +#include "Bitmap.h" +#include "Block.h" +#include "Event.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Game.h" +#include "Graphics.h" +#include "Model.h" +#include "World.h" +#include "Particle.h" +#include "Drawer2D.h" + +/*########################################################################################################################* +*------------------------------------------------------Entity Shadow------------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool shadows_boundTex; +static GfxResourceID shadows_VB; +static GfxResourceID shadows_tex; +static float shadow_radius, shadow_uvScale; +struct ShadowData { float y; BlockID block; cc_uint8 alpha; }; + +/* Circle shadows extend at most 4 blocks vertically */ +#define SHADOW_MAX_RANGE 4 +/* Circle shadows on blocks underneath the top block can be chopped up into at most 4 pieces */ +#define SHADOW_MAX_PER_SUB_BLOCK (4 * 4) +/* Circle shadows use at most: + - 4 vertices for top most block + - MAX_PER_SUB_BLOCK for everyblock underneath the top block */ +#define SHADOW_MAX_PER_COLUMN (4 + SHADOW_MAX_PER_SUB_BLOCK * (SHADOW_MAX_RANGE - 1)) +/* Circle shadows may be split across (x,z), (x,z+1), (x+1,z), (x+1,z+1) */ +#define SHADOW_MAX_VERTS 4 * SHADOW_MAX_PER_COLUMN + +static cc_bool lequal(float a, float b) { return a < b || Math_AbsF(a - b) < 0.001f; } +static void EntityShadow_DrawCoords(struct VertexTextured** vertices, struct Entity* e, struct ShadowData* data, float x1, float z1, float x2, float z2) { + PackedCol col; + struct VertexTextured* v; + Vec3 cen; + float u1, v1, u2, v2; + + if (lequal(x2, x1) || lequal(z2, z1)) return; + cen = e->Position; + + u1 = (x1 - cen.x) * shadow_uvScale + 0.5f; + v1 = (z1 - cen.z) * shadow_uvScale + 0.5f; + u2 = (x2 - cen.x) * shadow_uvScale + 0.5f; + v2 = (z2 - cen.z) * shadow_uvScale + 0.5f; + if (u2 <= 0.0f || v2 <= 0.0f || u1 >= 1.0f || v1 >= 1.0f) return; + + x1 = max(x1, cen.x - shadow_radius); u1 = u1 >= 0.0f ? u1 : 0.0f; + z1 = max(z1, cen.z - shadow_radius); v1 = v1 >= 0.0f ? v1 : 0.0f; + x2 = min(x2, cen.x + shadow_radius); u2 = u2 <= 1.0f ? u2 : 1.0f; + z2 = min(z2, cen.z + shadow_radius); v2 = v2 <= 1.0f ? v2 : 1.0f; + + v = *vertices; + col = PackedCol_Make(255, 255, 255, data->alpha); + + v->x = x1; v->y = data->y; v->z = z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = x2; v->y = data->y; v->z = z1; v->Col = col; v->U = u2; v->V = v1; v++; + v->x = x2; v->y = data->y; v->z = z2; v->Col = col; v->U = u2; v->V = v2; v++; + v->x = x1; v->y = data->y; v->z = z2; v->Col = col; v->U = u1; v->V = v2; v++; + + *vertices = v; +} + +static void EntityShadow_DrawSquareShadow(struct VertexTextured** vertices, float y, float x, float z) { + PackedCol col = PackedCol_Make(255, 255, 255, 220); + float uv1 = 63/128.0f, uv2 = 64/128.0f; + struct VertexTextured* v = *vertices; + + v->x = x; v->y = y; v->z = z; v->Col = col; v->U = uv1; v->V = uv1; v++; + v->x = x + 1; v->y = y; v->z = z; v->Col = col; v->U = uv2; v->V = uv1; v++; + v->x = x + 1; v->y = y; v->z = z + 1; v->Col = col; v->U = uv2; v->V = uv2; v++; + v->x = x; v->y = y; v->z = z + 1; v->Col = col; v->U = uv1; v->V = uv2; v++; + + *vertices = v; +} + +/* Shadow may extend down multiple blocks vertically */ +/* If so, shadow on a block must be 'chopped up' to avoid a shadow underneath block above this one */ +static void EntityShadow_DrawCircle(struct VertexTextured** vertices, struct Entity* e, struct ShadowData* data, float x, float z) { + Vec3 min, max, nMin, nMax; + int i; + x = (float)Math_Floor(x); z = (float)Math_Floor(z); + min = Blocks.MinBB[data[0].block]; max = Blocks.MaxBB[data[0].block]; + + EntityShadow_DrawCoords(vertices, e, &data[0], x + min.x, z + min.z, x + max.x, z + max.z); + for (i = 1; i < 4; i++) + { + if (data[i].block == BLOCK_AIR) return; + nMin = Blocks.MinBB[data[i].block]; nMax = Blocks.MaxBB[data[i].block]; + + EntityShadow_DrawCoords(vertices, e, &data[i], x + min.x, z + nMin.z, x + max.x, z + min.z); + EntityShadow_DrawCoords(vertices, e, &data[i], x + min.x, z + max.z, x + max.x, z + nMax.z); + + EntityShadow_DrawCoords(vertices, e, &data[i], x + nMin.x, z + nMin.z, x + min.x, z + nMax.z); + EntityShadow_DrawCoords(vertices, e, &data[i], x + max.x, z + nMin.z, x + nMax.x, z + nMax.z); + min = nMin; max = nMax; + } +} + +static void EntityShadow_CalcAlpha(float playerY, struct ShadowData* data) { + float height = playerY - data->y; + if (height <= 6.0f) { + data->alpha = (cc_uint8)(160 - 160 * height / 6.0f); + data->y += 1.0f / 64.0f; + return; + } + + data->alpha = 0; + if (height <= 16.0f) data->y += 1.0f / 64.0f; + else if (height <= 32.0f) data->y += 1.0f / 16.0f; + else if (height <= 96.0f) data->y += 1.0f / 8.0f; + else data->y += 1.0f / 4.0f; +} + +static cc_bool EntityShadow_GetBlocks(struct Entity* e, int x, int y, int z, struct ShadowData* data) { + struct ShadowData zeroData = { 0 }; + struct ShadowData* cur; + float posY, topY; + cc_bool outside; + BlockID block; cc_uint8 draw; + int i; + + for (i = 0; i < 4; i++) { data[i] = zeroData; } + cur = data; + posY = e->Position.y; + outside = !World_ContainsXZ(x, z); + + for (i = 0; y >= 0 && i < 4; y--) + { + if (!outside) { + block = World_GetBlock(x, y, z); + } else if (y == Env.EdgeHeight - 1) { + block = Blocks.Draw[Env.EdgeBlock] == DRAW_GAS ? BLOCK_AIR : BLOCK_BEDROCK; + } else if (y == Env_SidesHeight - 1) { + block = Blocks.Draw[Env.SidesBlock] == DRAW_GAS ? BLOCK_AIR : BLOCK_BEDROCK; + } else { + block = BLOCK_AIR; + } + + draw = Blocks.Draw[block]; + if (draw == DRAW_GAS || draw == DRAW_SPRITE || Blocks.IsLiquid[block]) continue; + topY = y + Blocks.MaxBB[block].y; + if (topY >= posY + 0.01f) continue; + + cur->block = block; cur->y = topY; + EntityShadow_CalcAlpha(posY, cur); + i++; cur++; + + /* Check if the casted shadow will continue on further down. */ + if (Blocks.MinBB[block].x == 0.0f && Blocks.MaxBB[block].x == 1.0f && + Blocks.MinBB[block].z == 0.0f && Blocks.MaxBB[block].z == 1.0f) return true; + } + + if (i < 4) { + cur->block = Env.EdgeBlock; cur->y = 0.0f; + EntityShadow_CalcAlpha(posY, cur); + i++; cur++; + } + return true; +} + +static void EntityShadow_Draw(struct Entity* e) { + struct VertexTextured vertices[128]; /* TODO this is less than maxVertes */ + struct VertexTextured* ptr; + struct ShadowData data[4]; + Vec3 pos; + float radius; + int y, count; + int x1, z1, x2, z2; + + pos = e->Position; + if (pos.y < 0.0f) return; + y = min((int)pos.y, World.MaxY); + + radius = 7.0f * min(e->ModelScale.y, 1.0f) * e->Model->shadowScale; + shadow_radius = radius / 16.0f; + shadow_uvScale = 16.0f / (radius * 2.0f); + + ptr = vertices; + if (Entities.ShadowsMode == SHADOW_MODE_SNAP_TO_BLOCK) { + x1 = Math_Floor(pos.x); z1 = Math_Floor(pos.z); + if (!EntityShadow_GetBlocks(e, x1, y, z1, data)) return; + + EntityShadow_DrawSquareShadow(&ptr, data[0].y, x1, z1); + } else { + x1 = Math_Floor(pos.x - shadow_radius); z1 = Math_Floor(pos.z - shadow_radius); + x2 = Math_Floor(pos.x + shadow_radius); z2 = Math_Floor(pos.z + shadow_radius); + + if (EntityShadow_GetBlocks(e, x1, y, z1, data) && data[0].alpha > 0) { + EntityShadow_DrawCircle(&ptr, e, data, (float)x1, (float)z1); + } + if (x1 != x2 && EntityShadow_GetBlocks(e, x2, y, z1, data) && data[0].alpha > 0) { + EntityShadow_DrawCircle(&ptr, e, data, (float)x2, (float)z1); + } + if (z1 != z2 && EntityShadow_GetBlocks(e, x1, y, z2, data) && data[0].alpha > 0) { + EntityShadow_DrawCircle(&ptr, e, data, (float)x1, (float)z2); + } + if (x1 != x2 && z1 != z2 && EntityShadow_GetBlocks(e, x2, y, z2, data) && data[0].alpha > 0) { + EntityShadow_DrawCircle(&ptr, e, data, (float)x2, (float)z2); + } + } + + if (ptr == vertices) return; + + if (!shadows_boundTex) { + Gfx_BindTexture(shadows_tex); + shadows_boundTex = true; + } + + count = (int)(ptr - vertices); + Gfx_SetDynamicVbData(shadows_VB, vertices, count); + Gfx_DrawVb_IndexedTris(count); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Entity Shadows------------------------------------------------------* +*#########################################################################################################################*/ +#define sh_size 128 +#define sh_half (sh_size / 2) + +static void EntityShadows_MakeTexture(void) { + BitmapCol pixels[sh_size * sh_size]; + BitmapCol color = BitmapCol_Make(0, 0, 0, 200); + struct Bitmap bmp; + cc_uint32 x, y; + + Bitmap_Init(bmp, sh_size, sh_size, pixels); + for (y = 0; y < sh_size; y++) { + BitmapCol* row = Bitmap_GetRow(&bmp, y); + + for (x = 0; x < sh_size; x++) { + float dist = + (sh_half - (x + 0.5f)) * (sh_half - (x + 0.5f)) + + (sh_half - (y + 0.5f)) * (sh_half - (y + 0.5f)); + row[x] = dist < sh_half * sh_half ? color : 0; + } + } + shadows_tex = Gfx_CreateTexture(&bmp, 0, false); +} + +void EntityShadows_Render(void) { + struct Entity* e; + int i; + if (Entities.ShadowsMode == SHADOW_MODE_NONE) return; + + shadows_boundTex = false; + if (!shadows_tex) + EntityShadows_MakeTexture(); + if (!shadows_VB) + shadows_VB = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, SHADOW_MAX_VERTS); + + Gfx_SetAlphaArgBlend(true); + Gfx_SetDepthWrite(false); + Gfx_SetAlphaBlending(true); + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + EntityShadow_Draw(&Entities.CurPlayer->Base); + + if (Entities.ShadowsMode == SHADOW_MODE_CIRCLE_ALL) { + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + e = Entities.List[i]; + if (!e || !e->ShouldRender || e == &Entities.CurPlayer->Base) continue; + EntityShadow_Draw(e); + } + } + + Gfx_SetAlphaArgBlend(false); + Gfx_SetDepthWrite(true); + Gfx_SetAlphaBlending(false); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Entity nametag------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID names_VB; +#define NAME_IS_EMPTY -30000 +#define NAME_OFFSET 3 /* offset of back layer of name above an entity */ + +static void MakeNameTexture(struct Entity* e) { + cc_string colorlessName; char colorlessBuffer[STRING_SIZE]; + BitmapCol shadowColor = BitmapCol_Make(80, 80, 80, 255); + BitmapCol origWhiteColor; + + struct DrawTextArgs args; + struct FontDesc font; + struct Context2D ctx; + int width, height; + cc_string name; + + /* Names are always drawn using default.png font */ + Font_MakeBitmapped(&font, 24, FONT_FLAGS_NONE); + /* Don't want DPI scaling or padding */ + font.size = 24; font.height = 24; + + name = String_FromRawArray(e->NameRaw); + DrawTextArgs_Make(&args, &name, &font, false); + width = Drawer2D_TextWidth(&args); + + if (!width) { + e->NameTex.ID = 0; + e->NameTex.x = NAME_IS_EMPTY; + } else { + String_InitArray(colorlessName, colorlessBuffer); + width += NAME_OFFSET; + height = Drawer2D_TextHeight(&args) + NAME_OFFSET; + + Context2D_Alloc(&ctx, width, height); + { + origWhiteColor = Drawer2D.Colors['f']; + + Drawer2D.Colors['f'] = shadowColor; + Drawer2D_WithoutColors(&colorlessName, &name); + args.text = colorlessName; + Context2D_DrawText(&ctx, &args, NAME_OFFSET, NAME_OFFSET); + + Drawer2D.Colors['f'] = origWhiteColor; + args.text = name; + Context2D_DrawText(&ctx, &args, 0, 0); + } + Context2D_MakeTexture(&e->NameTex, &ctx); + Context2D_Free(&ctx); + } +} + +static void DrawName(struct Entity* e) { + struct VertexTextured* vertices; + struct Model* model; + struct Matrix mat; + Vec3 pos; + float scale; + Vec2 size; + + if (!e->VTABLE->ShouldRenderName(e)) return; + if (e->NameTex.x == NAME_IS_EMPTY) return; + if (!e->NameTex.ID) MakeNameTexture(e); + Gfx_BindTexture(e->NameTex.ID); + + if (!names_VB) + names_VB = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, 4); + + model = e->Model; + Vec3_TransformY(&pos, model->GetNameY(e), &e->Transform); + + scale = e->ModelScale.y; + scale = scale > 1.0f ? (1.0f/70.0f) : (scale/70.0f); + size.x = e->NameTex.width * scale; size.y = e->NameTex.height * scale; + + if (Entities.NamesMode == NAME_MODE_ALL_UNSCALED && Entities.CurPlayer->Hacks.CanSeeAllNames) { + Matrix_Mul(&mat, &Gfx.View, &Gfx.Projection); /* TODO: This mul is slow, avoid it */ + /* Get W component of transformed position */ + scale = pos.x * mat.row1.w + pos.y * mat.row2.w + pos.z * mat.row3.w + mat.row4.w; + size.x *= scale * 0.2f; size.y *= scale * 0.2f; + } + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + + vertices = (struct VertexTextured*)Gfx_LockDynamicVb(names_VB, VERTEX_FORMAT_TEXTURED, 4); + Particle_DoRender(&size, &pos, &e->NameTex.uv, PACKEDCOL_WHITE, vertices); + Gfx_UnlockDynamicVb(names_VB); + + Gfx_DrawVb_IndexedTris(4); +} + +void EntityNames_Delete(struct Entity* e) { + Gfx_DeleteTexture(&e->NameTex.ID); + e->NameTex.x = 0; /* X is used as an 'empty name' flag */ +} + + +/*########################################################################################################################* +*-----------------------------------------------------Names rendering-----------------------------------------------------* +*#########################################################################################################################*/ +static int closestEntityId; + +void EntityNames_Render(void) { + struct LocalPlayer* p = Entities.CurPlayer; + cc_bool hadFog; + int i; + + if (Entities.NamesMode == NAME_MODE_NONE) return; + closestEntityId = Entities_GetClosest(&p->Base); + if (!p->Hacks.CanSeeAllNames || Entities.NamesMode != NAME_MODE_ALL) return; + + Gfx_SetAlphaTest(true); + hadFog = Gfx_GetFog(); + if (hadFog) Gfx_SetFog(false); + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + if (!Entities.List[i]) continue; + if (i != closestEntityId) DrawName(Entities.List[i]); + } + + Gfx_SetAlphaTest(false); + if (hadFog) Gfx_SetFog(true); +} + +void EntityNames_RenderHovered(void) { + struct LocalPlayer* p = Entities.CurPlayer; + struct Entity* e; + cc_bool allNames, hadFog; + cc_bool setupState = false; + int i; + + if (Entities.NamesMode == NAME_MODE_NONE) return; + allNames = !(Entities.NamesMode == NAME_MODE_HOVERED || Entities.NamesMode == NAME_MODE_ALL) + && p->Hacks.CanSeeAllNames; + + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + e = Entities.List[i]; + if (!e || e == &p->Base) continue; + if (!allNames && i != closestEntityId) continue; + + /* Only alter the GPU state when actually necessary */ + if (!setupState) { + Gfx_SetAlphaTest(true); + Gfx_SetDepthTest(false); + Gfx_SetDepthWrite(false); + + setupState = true; + hadFog = Gfx_GetFog(); + if (hadFog) Gfx_SetFog(false); + } + DrawName(e); + } + + if (!setupState) return; + Gfx_SetAlphaTest(false); + Gfx_SetDepthTest(true); + Gfx_SetDepthWrite(true); + if (hadFog) Gfx_SetFog(true); +} + +static void DeleteAllNameTextures(void) { + int i; + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + if (!Entities.List[i]) continue; + EntityNames_Delete(Entities.List[i]); + } +} + +static void EntityNames_ChatFontChanged(void* obj) { + DeleteAllNameTextures(); +} + + +/*########################################################################################################################* +*-----------------------------------------------Entity renderers component------------------------------------------------* +*#########################################################################################################################*/ +static void EntityRenderers_ContextLost(void* obj) { + Gfx_DeleteTexture(&shadows_tex); + Gfx_DeleteDynamicVb(&shadows_VB); + + Gfx_DeleteDynamicVb(&names_VB); + DeleteAllNameTextures(); +} + +static void EntityRenderers_Init(void) { + Event_Register_(&GfxEvents.ContextLost, NULL, EntityRenderers_ContextLost); + Event_Register_(&ChatEvents.FontChanged, NULL, EntityNames_ChatFontChanged); +} + +static void EntityRenderers_Free(void) { + EntityRenderers_ContextLost(NULL); +} + +struct IGameComponent EntityRenderers_Component = { + EntityRenderers_Init, /* Init */ + EntityRenderers_Free /* Free */ +}; diff --git a/src/EntityRenderers.h b/src/EntityRenderers.h new file mode 100644 index 0000000..75c71e3 --- /dev/null +++ b/src/EntityRenderers.h @@ -0,0 +1,21 @@ +#ifndef CC_ENTITYRENDERERS_H +#define CC_ENTITYRENDERERS_H +#include "Core.h" +/* Renders supporting objects for entities (shadows and names) + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent EntityRenderers_Component; +struct Entity; + +/* Draws shadows under entities, depending on Entities.ShadowsMode */ +void EntityShadows_Render(void); + +/* Deletes the texture containing the entity's nametag */ +void EntityNames_Delete(struct Entity* e); +/* Renders the name tags of entities, depending on Entities.NamesMode */ +void EntityNames_Render(void); +/* Renders hovered entity name tags (these appears through blocks) */ +void EntityNames_RenderHovered(void); + +#endif diff --git a/src/EnvRenderer.c b/src/EnvRenderer.c new file mode 100644 index 0000000..cb3071b --- /dev/null +++ b/src/EnvRenderer.c @@ -0,0 +1,958 @@ +#include "EnvRenderer.h" +#include "String.h" +#include "ExtMath.h" +#include "World.h" +#include "Funcs.h" +#include "Graphics.h" +#include "Physics.h" +#include "Block.h" +#include "Platform.h" +#include "Event.h" +#include "Utils.h" +#include "Game.h" +#include "Logger.h" +#include "Block.h" +#include "Event.h" +#include "TexturePack.h" +#include "Platform.h" +#include "Camera.h" +#include "Particle.h" +#include "Options.h" +#include "Entity.h" + +cc_bool EnvRenderer_Legacy, EnvRenderer_Minimal; + +static float CalcBlendFactor(float x) { + float blend = -0.13f + 0.28f * ((float)Math_Log2(x) * 0.17329f); + if (blend < 0.0f) blend = 0.0f; + if (blend > 1.0f) blend = 1.0f; + return blend; +} + +#define EnvRenderer_AxisSize() (EnvRenderer_Legacy ? 128 : 2048) +/* Returns the number of vertices needed to subdivide a quad */ +static int CalcNumVertices(int axis1Len, int axis2Len) { + int axisSize = EnvRenderer_AxisSize(); + return Math_CeilDiv(axis1Len, axisSize) * Math_CeilDiv(axis2Len, axisSize) * 4; +} + + +/*########################################################################################################################* +*------------------------------------------------------------Fog----------------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool CameraInsideBlock(BlockID block, IVec3* coords) { + struct AABB blockBB; + Vec3 pos; + IVec3_ToVec3(&pos, coords); /* pos = coords; */ + + Vec3_Add(&blockBB.Min, &pos, &Blocks.MinBB[block]); + Vec3_Add(&blockBB.Max, &pos, &Blocks.MaxBB[block]); + return AABB_ContainsPoint(&blockBB, &Camera.CurrentPos); +} + +static void CalcFog(float* density, PackedCol* color) { + IVec3 coords; + BlockID block; + float blend; + + IVec3_Floor(&coords, &Camera.CurrentPos); /* coords = floor(camera_pos); */ + block = World_SafeGetBlock(coords.x, coords.y, coords.z); + + if (Blocks.FogDensity[block] && CameraInsideBlock(block, &coords)) { + *density = Blocks.FogDensity[block]; + *color = Blocks.FogCol[block]; + } else { + *density = 0.0f; + /* Blend fog and sky together */ + blend = CalcBlendFactor((float)Game_ViewDistance); + *color = PackedCol_Lerp(Env.FogCol, Env.SkyCol, blend); + } +} + +static void UpdateFogMinimal(float fogDensity) { + int dist; + /* TODO: rewrite this to avoid raising the event? want to avoid recreating vbos too many times often */ + + if (fogDensity != 0.0f) { + /* Exp fog mode: f = e^(-density*coord) */ + /* Solve coord for f = 0.05 (good approx for fog end) */ + /* i.e. log(0.05) = -density * coord */ + #define LOG_005 -2.99573227355399f + + dist = (int)(LOG_005 / -fogDensity); + Game_SetViewDistance(min(dist, Game_UserViewDistance)); + } else { + Game_SetViewDistance(Game_UserViewDistance); + } +} + +static void UpdateFogNormal(float fogDensity, PackedCol fogColor) { + float density; + + if (fogDensity != 0.0f) { + Gfx_SetFogMode(FOG_EXP); + Gfx_SetFogDensity(fogDensity); + } else if (Env.ExpFog) { + Gfx_SetFogMode(FOG_EXP); + /* f = 1-z/end f = e^(-dz) + solve for f = 0.01 gives: + e^(-dz)=0.01 --> -dz=ln(0.01) + 0.99=z/end --> z=end*0.99 + therefore + d = -ln(0.01)/(end*0.99) */ + #define LOG_001 -4.60517018598809f + + density = -LOG_001 / (Game_ViewDistance * 0.99f); + Gfx_SetFogDensity(density); + } else { + Gfx_SetFogMode(FOG_LINEAR); + Gfx_SetFogEnd((float)Game_ViewDistance); + } + Gfx_SetFogCol(fogColor); + Game_SetViewDistance(Game_UserViewDistance); +} + +void EnvRenderer_UpdateFog(void) { + float fogDensity; + PackedCol fogColor; + if (!World.Loaded) return; + + CalcFog(&fogDensity, &fogColor); + Gfx_ClearColor(fogColor); + + if (EnvRenderer_Minimal) { + UpdateFogMinimal(fogDensity); + } else { + UpdateFogNormal(fogDensity, fogColor); + } +} + + +/*########################################################################################################################* +*----------------------------------------------------------Clouds---------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID clouds_vb, clouds_tex; +static int clouds_vertices; + +void EnvRenderer_RenderClouds(void) { + float offset; + if (!clouds_vb || Env.CloudsHeight < -2000) return; + offset = (float)(Game.Time / 2048.0f * 0.6f * Env.CloudsSpeed); + + Gfx_EnableTextureOffset(offset, 0); + Gfx_SetAlphaTest(true); + Gfx_BindTexture(clouds_tex); + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Gfx_BindVb(clouds_vb); + Gfx_DrawVb_IndexedTris(clouds_vertices); + Gfx_SetAlphaTest(false); + Gfx_DisableTextureOffset(); +} + +static void DrawCloudsY(int x1, int z1, int x2, int z2, int y, struct VertexTextured* v) { + int endX = x2, endZ = z2, startZ = z1, axisSize = EnvRenderer_AxisSize(); + float u1, u2, v1, v2; + float yy = (float)y + 0.1f; + PackedCol col = Env.CloudsCol; + /* adjust range so that largest negative uv coordinate is shifted to 0 or above. */ + float offset = (float)Math_CeilDiv(-x1, 2048); + + for (; x1 < endX; x1 += axisSize) { + x2 = x1 + axisSize; + if (x2 > endX) x2 = endX; + + for (z1 = startZ; z1 < endZ; z1 += axisSize) { + z2 = z1 + axisSize; + if (z2 > endZ) z2 = endZ; + + u1 = (float)x1 / 2048.0f + offset; u2 = (float)x2 / 2048.0f + offset; + v1 = (float)z1 / 2048.0f + offset; v2 = (float)z2 / 2048.0f + offset; + + v->x = (float)x1; v->y = yy; v->z = (float)z1; v->Col = col; v->U = u1; v->V = v1; v++; + v->x = (float)x1; v->y = yy; v->z = (float)z2; v->Col = col; v->U = u1; v->V = v2; v++; + v->x = (float)x2; v->y = yy; v->z = (float)z2; v->Col = col; v->U = u2; v->V = v2; v++; + v->x = (float)x2; v->y = yy; v->z = (float)z1; v->Col = col; v->U = u2; v->V = v1; v++; + } + } +} + +static void UpdateClouds(void) { + struct VertexTextured* data; + int extent; + int x1, z1, x2, z2; + + Gfx_DeleteVb(&clouds_vb); + if (!World.Loaded || Gfx.LostContext) return; + if (EnvRenderer_Minimal) return; + + extent = Utils_AdjViewDist(Game_ViewDistance); + x1 = -extent; x2 = World.Width + extent; + z1 = -extent; z2 = World.Length + extent; + clouds_vertices = CalcNumVertices(x2 - x1, z2 - z1); + + data = (struct VertexTextured*)Gfx_RecreateAndLockVb(&clouds_vb, + VERTEX_FORMAT_TEXTURED, clouds_vertices); + DrawCloudsY(x1, z1, x2, z2, Env.CloudsHeight, data); + Gfx_UnlockVb(clouds_vb); +} + + +/*########################################################################################################################* +*------------------------------------------------------------Sky----------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID sky_vb; +static int sky_vertices; + +void EnvRenderer_RenderSky(void) { + struct Matrix m; + float skyY, normY, dy; + if (!sky_vb || EnvRenderer_ShouldRenderSkybox()) return; + + normY = (float)World.Height + 8.0f; + skyY = max(Camera.CurrentPos.y + 8.0f, normY); + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + Gfx_BindVb(sky_vb); + + if (skyY == normY) { + Gfx_DrawVb_IndexedTris(sky_vertices); + } else { + m = Gfx.View; + dy = skyY - normY; + /* inlined Y translation matrix multiply */ + m.row4.x += dy * m.row2.x; m.row4.y += dy * m.row2.y; + m.row4.z += dy * m.row2.z; m.row4.w += dy * m.row2.w; + + Gfx_LoadMatrix(MATRIX_VIEW, &m); + Gfx_DrawVb_IndexedTris(sky_vertices); + Gfx_LoadMatrix(MATRIX_VIEW, &Gfx.View); + } +} + +static void DrawSkyY(int x1, int z1, int x2, int z2, int y, struct VertexColoured* v) { + int endX = x2, endZ = z2, startZ = z1, axisSize = EnvRenderer_AxisSize(); + PackedCol col = Env.SkyCol; + + for (; x1 < endX; x1 += axisSize) { + x2 = x1 + axisSize; + if (x2 > endX) x2 = endX; + + for (z1 = startZ; z1 < endZ; z1 += axisSize) { + z2 = z1 + axisSize; + if (z2 > endZ) z2 = endZ; + + v->x = (float)x1; v->y = (float)y; v->z = (float)z1; v->Col = col; v++; + v->x = (float)x1; v->y = (float)y; v->z = (float)z2; v->Col = col; v++; + v->x = (float)x2; v->y = (float)y; v->z = (float)z2; v->Col = col; v++; + v->x = (float)x2; v->y = (float)y; v->z = (float)z1; v->Col = col; v++; + } + } +} + +static void UpdateSky(void) { + struct VertexColoured* data; + int extent, height; + int x1, z1, x2, z2; + + Gfx_DeleteVb(&sky_vb); + if (!World.Loaded || Gfx.LostContext) return; + if (EnvRenderer_Minimal) return; + + extent = Utils_AdjViewDist(Game_ViewDistance); + x1 = -extent; x2 = World.Width + extent; + z1 = -extent; z2 = World.Length + extent; + sky_vertices = CalcNumVertices(x2 - x1, z2 - z1); + + data = (struct VertexColoured*)Gfx_RecreateAndLockVb(&sky_vb, + VERTEX_FORMAT_COLOURED, sky_vertices); + height = max((World.Height + 2), Env.CloudsHeight) + 6; + DrawSkyY(x1, z1, x2, z2, height, data); + Gfx_UnlockVb(sky_vb); +} + +/*########################################################################################################################* +*----------------------------------------------------------Skybox---------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID skybox_tex, skybox_vb; +#define SKYBOX_COUNT (6 * 4) +cc_bool EnvRenderer_ShouldRenderSkybox(void) { return skybox_tex && !EnvRenderer_Minimal; } + +static void AllocateSkyboxVB(void) { + static const struct VertexTextured vertices[SKYBOX_COUNT] = { + /* Front quad */ + { -1, -1, -1, 0, 0.25f, 1.00f }, { 1, -1, -1, 0, 0.50f, 1.00f }, + { 1, 1, -1, 0, 0.50f, 0.50f }, { -1, 1, -1, 0, 0.25f, 0.50f }, + /* Left quad */ + { -1, -1, 1, 0, 0.00f, 1.00f }, { -1, -1, -1, 0, 0.25f, 1.00f }, + { -1, 1, -1, 0, 0.25f, 0.50f }, { -1, 1, 1, 0, 0.00f, 0.50f }, + /* Back quad */ + { 1, -1, 1, 0, 0.75f, 1.00f }, { -1, -1, 1, 0, 1.00f, 1.00f }, + { -1, 1, 1, 0, 1.00f, 0.50f }, { 1, 1, 1, 0, 0.75f, 0.50f }, + /* Right quad */ + { 1, -1, -1, 0, 0.50f, 1.00f }, { 1, -1, 1, 0, 0.75f, 1.00f }, + { 1, 1, 1, 0, 0.75f, 0.50f }, { 1, 1, -1, 0, 0.50f, 0.50f }, + /* Top quad */ + { 1, 1, -1, 0, 0.50f, 0.50f }, { 1, 1, 1, 0, 0.50f, 0.00f }, + { -1, 1, 1, 0, 0.25f, 0.00f }, { -1, 1, -1, 0, 0.25f, 0.50f }, + /* Bottom quad */ + { 1, -1, -1, 0, 0.75f, 0.50f }, { 1, -1, 1, 0, 0.75f, 0.00f }, + { -1, -1, 1, 0, 0.50f, 0.00f }, { -1, -1, -1, 0, 0.50f, 0.50f }, + }; + struct VertexTextured* data; + int i; + + data = (struct VertexTextured*)Gfx_RecreateAndLockVb(&skybox_vb, + VERTEX_FORMAT_TEXTURED, SKYBOX_COUNT); + Mem_Copy(data, vertices, sizeof(vertices)); + for (i = 0; i < SKYBOX_COUNT; i++) { data[i].Col = Env.SkyboxCol; } + Gfx_UnlockVb(skybox_vb); +} + +void EnvRenderer_RenderSkybox(void) { + struct Matrix m, rotX, rotY, view; + float rotTime; + Vec3 pos; + if (!skybox_vb) AllocateSkyboxVB(); + + Gfx_SetDepthWrite(false); + Gfx_BindTexture(skybox_tex); + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + + /* Base skybox rotation */ + rotTime = (float)(Game.Time * 2 * MATH_PI); /* So speed of 1 rotates whole skybox every second */ + Matrix_RotateY(&rotY, Env.SkyboxHorSpeed * rotTime); + Matrix_RotateX(&rotX, Env.SkyboxVerSpeed * rotTime); + Matrix_Mul(&m, &rotY, &rotX); + + /* Rotate around camera */ + pos = Camera.CurrentPos; + Vec3_Set(Camera.CurrentPos, 0,0,0); + Camera.Active->GetView(&view); + Matrix_MulBy(&m, &view); + Camera.CurrentPos = pos; + + Gfx_LoadMatrix(MATRIX_VIEW, &m); + Gfx_BindVb(skybox_vb); + Gfx_DrawVb_IndexedTris(SKYBOX_COUNT); + + Gfx_LoadMatrix(MATRIX_VIEW, &Gfx.View); + Gfx_SetDepthWrite(true); +} + +/*########################################################################################################################* +*----------------------------------------------------------Weather--------------------------------------------------------* +*#########################################################################################################################*/ +cc_int16* Weather_Heightmap; +static GfxResourceID rain_tex, snow_tex, weather_vb; +static float weather_accumulator; +static IVec3 lastPos; + +#define WEATHER_EXTENT 4 +#define WEATHER_VERTS 8 /* 2 quads per tile */ +#define WEATHER_RANGE (WEATHER_EXTENT * 2 + 1) + +#define WEATHER_VERTS_COUNT WEATHER_RANGE * WEATHER_RANGE * WEATHER_VERTS +#define Weather_Pack(x, z) ((x) * World.Length + (z)) + +static void InitWeatherHeightmap(void) { + int i; + Weather_Heightmap = (cc_int16*)Mem_Alloc(World.Width * World.Length, 2, "weather heightmap"); + + for (i = 0; i < World.Width * World.Length; i++) { + Weather_Heightmap[i] = Int16_MaxValue; + } +} + +#define RainCalcBody(get_block)\ +for (y = maxY; y >= 0; y--, i -= World.OneY) {\ + draw = Blocks.Draw[get_block];\ +\ + if (!(draw == DRAW_GAS || draw == DRAW_SPRITE)) {\ + Weather_Heightmap[hIndex] = y;\ + return y;\ + }\ +} + +static int CalcRainHeightAt(int x, int maxY, int z, int hIndex) { + int i = World_Pack(x, maxY, z), y; + cc_uint8 draw; + +#ifndef EXTENDED_BLOCKS + RainCalcBody(World.Blocks[i]); +#else + if (World.IDMask <= 0xFF) { + RainCalcBody(World.Blocks[i]); + } else { + RainCalcBody(World.Blocks[i] | (World.Blocks2[i] << 8)); + } +#endif + + Weather_Heightmap[hIndex] = -1; + return -1; +} + +static float GetRainHeight(int x, int z) { + int hIndex, height; + int y; + if (!World_ContainsXZ(x, z)) return (float)Env.EdgeHeight; + + hIndex = Weather_Pack(x, z); + height = Weather_Heightmap[hIndex]; + + y = height == Int16_MaxValue ? CalcRainHeightAt(x, World.MaxY, z, hIndex) : height; + return y == -1 ? 0 : y + Blocks.MaxBB[World_GetBlock(x, y, z)].y; +} + +void EnvRenderer_OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock) { + cc_bool didBlock = !(Blocks.Draw[oldBlock] == DRAW_GAS || Blocks.Draw[oldBlock] == DRAW_SPRITE); + cc_bool nowBlock = !(Blocks.Draw[newBlock] == DRAW_GAS || Blocks.Draw[newBlock] == DRAW_SPRITE); + int hIndex, height; + if (didBlock == nowBlock) return; + + hIndex = Weather_Pack(x, z); + height = Weather_Heightmap[hIndex]; + /* Two cases can be skipped here: */ + /* a) rain height was not calculated to begin with (height is short.MaxValue) */ + /* b) changed y is below current calculated rain height */ + if (y < height) return; + + if (nowBlock) { + /* Simple case: Rest of column below is now not visible to rain. */ + Weather_Heightmap[hIndex] = y; + } else { + /* Part of the column is now visible to rain, we don't know how exactly how high it should be though. */ + /* However, we know that if the old block was above or equal to rain height, then the new rain height must be <= old block.y */ + CalcRainHeightAt(x, y, z, hIndex); + } +} + +static float CalcRainAlphaAt(float x) { + /* Wolfram Alpha: fit {0,178},{1,169},{4,147},{9,114},{16,59},{25,9} */ + float falloff = 0.05f * x * x - 7 * x; + return 178 + falloff * Env.WeatherFade; +} + +struct RainCoord { int dx, dz; float y; }; +static RNGState snowDirRng; + +void EnvRenderer_RenderWeather(float delta) { + struct RainCoord coords[WEATHER_RANGE * WEATHER_RANGE]; + int i, weather, numCoords = 0; + struct VertexTextured* v; + cc_bool moved, particles; + float speed, vOffsetBase, vOffset; + IVec3 pos; + + PackedCol color; + int dist, dx, dz, x, z; + float alpha, y, height; + float uOffset1, uOffset2, uSpeed; + float worldV, v1, v2, vPlane1Offset; + float x1,y1,z1, x2,y2,z2; + + weather = Env.Weather; + if (weather == WEATHER_SUNNY) return; + + if (!Weather_Heightmap) + InitWeatherHeightmap(); + if (!weather_vb) + weather_vb = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, WEATHER_VERTS_COUNT); + + IVec3_Floor(&pos, &Camera.CurrentPos); + moved = pos.x != lastPos.x || pos.y != lastPos.y || pos.z != lastPos.z; + lastPos = pos; + + /* Rain should extend up by 64 blocks, or to the top of the world. */ + pos.y += 64; + pos.y = max(World.Height, pos.y); + + weather_accumulator += delta; + particles = weather == WEATHER_RAINY && (weather_accumulator >= 0.25f || moved); + + for (dx = -WEATHER_EXTENT; dx <= WEATHER_EXTENT; dx++) { + for (dz = -WEATHER_EXTENT; dz <= WEATHER_EXTENT; dz++) { + x = pos.x + dx; z = pos.z + dz; + + y = GetRainHeight(x, z); + if (pos.y <= y) continue; + if (particles) Particles_RainSnowEffect((float)x, y, (float)z); + + coords[numCoords].dx = dx; + coords[numCoords].y = y; + coords[numCoords].dz = dz; + numCoords++; + } + } + + Gfx_BindTexture(weather == WEATHER_RAINY ? rain_tex : snow_tex); + if (particles) weather_accumulator = 0; + if (!numCoords) return; + + Gfx_SetAlphaTest(false); + Gfx_SetDepthWrite(false); + Gfx_SetAlphaArgBlend(true); + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + v = (struct VertexTextured*)Gfx_LockDynamicVb(weather_vb, + VERTEX_FORMAT_TEXTURED, numCoords * WEATHER_VERTS); + + color = Env.SunCol; + speed = (weather == WEATHER_RAINY ? 1.0f : 0.2f) * Env.WeatherSpeed; + + vOffsetBase = (float)Game.Time * speed; + vPlane1Offset = weather == WEATHER_RAINY ? 0 : 0.25f; /* Offset v on 1 plane while snowing to avoid the unnatural mirrored texture effect */ + + for (i = 0; i < numCoords; i++) + { + dx = coords[i].dx; + y = coords[i].y; + dz = coords[i].dz; + + height = pos.y - y; + + dist = dx * dx + dz * dz; + alpha = CalcRainAlphaAt((float)dist); + Math_Clamp(alpha, 0.0f, 255.0f); + color = (color & PACKEDCOL_RGB_MASK) | PackedCol_A_Bits(alpha); + + x = dx + pos.x; + z = dz + pos.z; + + uOffset1 = 0; + uOffset2 = 0; + if (weather == WEATHER_SNOWY) { + Random_Seed(&snowDirRng, (x + 1217 * z) & 0x7fffffff); + + /* Multiply horizontal speed by a random float from -1 to 1 */ + uSpeed = (float)Game.Time * Env.WeatherSpeed * 0.5f; + uOffset1 = uSpeed * (Random_Float(&snowDirRng) * 2 + -1); + uOffset2 = uSpeed * (Random_Float(&snowDirRng) * 2 + -1); + + /* Multiply vertical speed by a random float from 1.0 to 0.25 */ + vOffset = vOffsetBase * (float)(Random_Float(&snowDirRng) * (1.0f - 0.25f) + 0.25f); + } else { + vOffset = vOffsetBase; + } + + worldV = vOffset + (z & 1) / 2.0f - (x & 0x0F) / 16.0f; + v1 = y / 6.0f + worldV; + v2 = (y + height) / 6.0f + worldV; + x1 = (float)x; y1 = (float)y; z1 = (float)z; + x2 = (float)(x + 1); y2 = (float)(y + height); z2 = (float)(z + 1); + + v->x = x1; v->y = y1; v->z = z1; v->Col = color; v->U = uOffset1; v->V = v1 + vPlane1Offset; v++; + v->x = x1; v->y = y2; v->z = z1; v->Col = color; v->U = uOffset1; v->V = v2 + vPlane1Offset; v++; + v->x = x2; v->y = y2; v->z = z2; v->Col = color; v->U = uOffset1 + 1.0f; v->V = v2 + vPlane1Offset; v++; + v->x = x2; v->y = y1; v->z = z2; v->Col = color; v->U = uOffset1 + 1.0f; v->V = v1 + vPlane1Offset; v++; + + v->x = x2; v->y = y1; v->z = z1; v->Col = color; v->U = uOffset2 + 1.0f; v->V = v1; v++; + v->x = x2; v->y = y2; v->z = z1; v->Col = color; v->U = uOffset2 + 1.0f; v->V = v2; v++; + v->x = x1; v->y = y2; v->z = z2; v->Col = color; v->U = uOffset2; v->V = v2; v++; + v->x = x1; v->y = y1; v->z = z2; v->Col = color; v->U = uOffset2; v->V = v1; v++; + } + + Gfx_UnlockDynamicVb(weather_vb); + Gfx_DrawVb_IndexedTris(numCoords * WEATHER_VERTS); + + Gfx_SetAlphaArgBlend(false); + Gfx_SetDepthWrite(true); + Gfx_SetAlphaTest(false); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Sides/Edge-------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID sides_vb, edges_vb, sides_tex, edges_tex; +static int sides_vertices, edges_vertices; +static cc_bool sides_fullBright, edges_fullBright; +static TextureLoc edges_lastTexLoc, sides_lastTexLoc; + +static void RenderBorders(BlockID block, GfxResourceID vb, GfxResourceID tex, int count) { + if (!vb) return; + + Gfx_SetupAlphaState(Blocks.Draw[block]); + Gfx_EnableMipmaps(); + + Gfx_BindTexture(tex); + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Gfx_BindVb(vb); + Gfx_DrawVb_IndexedTris(count); + + Gfx_DisableMipmaps(); + Gfx_RestoreAlphaState(Blocks.Draw[block]); +} + +void EnvRenderer_RenderMapSides(void) { + RenderBorders(Env.SidesBlock, sides_vb, sides_tex, sides_vertices); +} + +void EnvRenderer_RenderMapEdges(void) { + /* Do not draw water when player cannot see it */ + /* Fixes some 'depth bleeding through' issues with 16 bit depth buffers on large maps */ + int yVisible = min(0, Env_SidesHeight); + if (Camera.CurrentPos.y < yVisible && sides_vb) return; + + RenderBorders(Env.EdgeBlock, edges_vb, edges_tex, edges_vertices); +} + +static void MakeBorderTex(GfxResourceID* texId, BlockID block) { + TextureLoc loc = Block_Tex(block, FACE_YMAX); + if (Gfx.LostContext) return; + + Gfx_DeleteTexture(texId); + *texId = Atlas2D_LoadTile(loc); +} + +static Rect2D EnvRenderer_Rect(int x, int y, int width, int height) { + Rect2D r; + r.x = x; r.y = y; r.width = width; r.height = height; + return r; +} + +static void CalcBorderRects(Rect2D* rects) { + int extent = Utils_AdjViewDist(Game_ViewDistance); + rects[0] = EnvRenderer_Rect(-extent, -extent, extent + World.Width + extent, extent); + rects[1] = EnvRenderer_Rect(-extent, World.Length, extent + World.Width + extent, extent); + + rects[2] = EnvRenderer_Rect(-extent, 0, extent, World.Length); + rects[3] = EnvRenderer_Rect(World.Width, 0, extent, World.Length); +} + +static void UpdateBorderTextures(void) { + MakeBorderTex(&edges_tex, Env.EdgeBlock); + MakeBorderTex(&sides_tex, Env.SidesBlock); +} + +#define Borders_HorOffset(block) (Blocks.RenderMinBB[block].x - Blocks.MinBB[block].x) +#define Borders_YOffset(block) (Blocks.RenderMinBB[block].y - Blocks.MinBB[block].y) + +static void DrawBorderX(int x, int z1, int z2, int y1, int y2, PackedCol color, struct VertexTextured** vertices) { + int endZ = z2, endY = y2, startY = y1, axisSize = EnvRenderer_AxisSize(); + float u2, v2; + struct VertexTextured* v = *vertices; + + for (; z1 < endZ; z1 += axisSize) { + z2 = z1 + axisSize; + if (z2 > endZ) z2 = endZ; + + for (y1 = startY; y1 < endY; y1 += axisSize) { + y2 = y1 + axisSize; + if (y2 > endY) y2 = endY; + + u2 = (float)z2 - (float)z1; v2 = (float)y2 - (float)y1; + v->x = (float)x; v->y = (float)y1; v->z = (float)z1; v->Col = color; v->U = 0; v->V = v2; v++; + v->x = (float)x; v->y = (float)y2; v->z = (float)z1; v->Col = color; v->U = 0; v->V = 0; v++; + v->x = (float)x; v->y = (float)y2; v->z = (float)z2; v->Col = color; v->U = u2; v->V = 0; v++; + v->x = (float)x; v->y = (float)y1; v->z = (float)z2; v->Col = color; v->U = u2; v->V = v2; v++; + } + } + *vertices = v; +} + +static void DrawBorderZ(int z, int x1, int x2, int y1, int y2, PackedCol color, struct VertexTextured** vertices) { + int endX = x2, endY = y2, startY = y1, axisSize = EnvRenderer_AxisSize(); + float u2, v2; + struct VertexTextured* v = *vertices; + + for (; x1 < endX; x1 += axisSize) { + x2 = x1 + axisSize; + if (x2 > endX) x2 = endX; + + for (y1 = startY; y1 < endY; y1 += axisSize) { + y2 = y1 + axisSize; + if (y2 > endY) y2 = endY; + + u2 = (float)x2 - (float)x1; v2 = (float)y2 - (float)y1; + v->x = (float)x1; v->y = (float)y1; v->z = (float)z; v->Col = color; v->U = 0; v->V = v2; v++; + v->x = (float)x1; v->y = (float)y2; v->z = (float)z; v->Col = color; v->U = 0; v->V = 0; v++; + v->x = (float)x2; v->y = (float)y2; v->z = (float)z; v->Col = color; v->U = u2; v->V = 0; v++; + v->x = (float)x2; v->y = (float)y1; v->z = (float)z; v->Col = color; v->U = u2; v->V = v2; v++; + } + } + *vertices = v; +} + +static void DrawBorderY(int x1, int z1, int x2, int z2, float y, PackedCol color, float offset, float yOffset, struct VertexTextured** vertices) { + int endX = x2, endZ = z2, startZ = z1, axisSize = EnvRenderer_AxisSize(); + float u2, v2; + struct VertexTextured* v = *vertices; + float yy = y + yOffset; + + for (; x1 < endX; x1 += axisSize) { + x2 = x1 + axisSize; + if (x2 > endX) x2 = endX; + + for (z1 = startZ; z1 < endZ; z1 += axisSize) { + z2 = z1 + axisSize; + if (z2 > endZ) z2 = endZ; + + u2 = (float)x2 - (float)x1; v2 = (float)z2 - (float)z1; + v->x = (float)x1 + offset; v->y = yy; v->z = (float)z1 + offset; v->Col = color; v->U = 0; v->V = 0; v++; + v->x = (float)x1 + offset; v->y = yy; v->z = (float)z2 + offset; v->Col = color; v->U = 0; v->V = v2; v++; + v->x = (float)x2 + offset; v->y = yy; v->z = (float)z2 + offset; v->Col = color; v->U = u2; v->V = v2; v++; + v->x = (float)x2 + offset; v->y = yy; v->z = (float)z1 + offset; v->Col = color; v->U = u2; v->V = 0; v++; + } + } + *vertices = v; +} + +static void UpdateMapSides(void) { + Rect2D rects[4], r; + BlockID block; + PackedCol color; + int y, y1, y2; + int i; + struct VertexTextured* data; + + Gfx_DeleteVb(&sides_vb); + if (!World.Loaded || Gfx.LostContext) return; + block = Env.SidesBlock; + + if (Blocks.Draw[block] == DRAW_GAS) return; + CalcBorderRects(rects); + + sides_vertices = 0; + for (i = 0; i < 4; i++) { + r = rects[i]; + sides_vertices += CalcNumVertices(r.width, r.height); /* YQuads outside */ + } + + y = Env_SidesHeight; + sides_vertices += CalcNumVertices(World.Width, World.Length); /* YQuads beneath map */ + sides_vertices += 2 * CalcNumVertices(World.Width, Math_AbsI(y)); /* ZQuads */ + sides_vertices += 2 * CalcNumVertices(World.Length, Math_AbsI(y)); /* XQuads */ + data = (struct VertexTextured*)Gfx_RecreateAndLockVb(&sides_vb, + VERTEX_FORMAT_TEXTURED, sides_vertices); + + sides_fullBright = Blocks.Brightness[block]; + color = sides_fullBright ? PACKEDCOL_WHITE : Env.ShadowCol; + Block_Tint(color, block) + + for (i = 0; i < 4; i++) { + r = rects[i]; + DrawBorderY(r.x, r.y, r.x + r.width, r.y + r.height, (float)y, color, + 0, Borders_YOffset(block), &data); + } + + /* Work properly for when ground level is below 0 */ + y1 = 0; y2 = y; + if (y < 0) { y1 = y; y2 = 0; } + + DrawBorderY(0, 0, World.Width, World.Length, 0, color, 0, 0, &data); + DrawBorderZ(0, 0, World.Width, y1, y2, color, &data); + DrawBorderZ(World.Length, 0, World.Width, y1, y2, color, &data); + DrawBorderX(0, 0, World.Length, y1, y2, color, &data); + DrawBorderX(World.Width, 0, World.Length, y1, y2, color, &data); + + Gfx_UnlockVb(sides_vb); +} + +static void UpdateMapEdges(void) { + Rect2D rects[4], r; + BlockID block; + PackedCol color; + float y; + int i; + struct VertexTextured* data; + + Gfx_DeleteVb(&edges_vb); + if (!World.Loaded || Gfx.LostContext) return; + block = Env.EdgeBlock; + + if (Blocks.Draw[block] == DRAW_GAS) return; + CalcBorderRects(rects); + + edges_vertices = 0; + for (i = 0; i < 4; i++) { + r = rects[i]; + edges_vertices += CalcNumVertices(r.width, r.height); /* YPlanes outside */ + } + data = (struct VertexTextured*)Gfx_RecreateAndLockVb(&edges_vb, + VERTEX_FORMAT_TEXTURED, edges_vertices); + + edges_fullBright = Blocks.Brightness[block]; + color = edges_fullBright ? PACKEDCOL_WHITE : Env.SunCol; + Block_Tint(color, block) + + y = (float)Env.EdgeHeight; + for (i = 0; i < 4; i++) { + r = rects[i]; + DrawBorderY(r.x, r.y, r.x + r.width, r.y + r.height, y, color, + Borders_HorOffset(block), Borders_YOffset(block), &data); + } + Gfx_UnlockVb(edges_vb); +} + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static void CloudsPngProcess(struct Stream* stream, const cc_string* name) { + Game_UpdateTexture(&clouds_tex, stream, name, NULL, NULL); +} +static struct TextureEntry clouds_entry = { "clouds.png", CloudsPngProcess }; + +static void SkyboxPngProcess(struct Stream* stream, const cc_string* name) { + Game_UpdateTexture(&skybox_tex, stream, name, NULL, NULL); +} +static struct TextureEntry skybox_entry = { "skybox.png", SkyboxPngProcess }; + +static void SnowPngProcess(struct Stream* stream, const cc_string* name) { + Game_UpdateTexture(&snow_tex, stream, name, NULL, NULL); +} +static struct TextureEntry snow_entry = { "snow.png", SnowPngProcess }; + +static void RainPngProcess(struct Stream* stream, const cc_string* name) { + Game_UpdateTexture(&rain_tex, stream, name, NULL, NULL); +} +static struct TextureEntry rain_entry = { "rain.png", RainPngProcess }; + + +static void DeleteVbs(void) { + Gfx_DeleteVb(&sky_vb); + Gfx_DeleteVb(&clouds_vb); + Gfx_DeleteVb(&skybox_vb); + Gfx_DeleteVb(&sides_vb); + Gfx_DeleteVb(&edges_vb); + Gfx_DeleteDynamicVb(&weather_vb); +} + +static void OnContextLost(void* obj) { + DeleteVbs(); + Gfx_DeleteTexture(&sides_tex); + Gfx_DeleteTexture(&edges_tex); + + if (Gfx.ManagedTextures) return; + Gfx_DeleteTexture(&clouds_tex); + Gfx_DeleteTexture(&skybox_tex); + Gfx_DeleteTexture(&rain_tex); + Gfx_DeleteTexture(&snow_tex); +} + +static void UpdateAll(void) { + UpdateMapSides(); + UpdateMapEdges(); + UpdateClouds(); + UpdateSky(); + Gfx_DeleteVb(&skybox_vb); + EnvRenderer_UpdateFog(); + + Gfx_DeleteDynamicVb(&weather_vb); + /* TODO: Unnecessary to delete the weather VB? */ + if (Gfx.LostContext) return; + /* TODO: Don't need to do this on every new map */ + UpdateBorderTextures(); +} + +static void OnContextRecreated(void* obj) { + Gfx_SetFog(!EnvRenderer_Minimal); + UpdateAll(); +} + +void EnvRenderer_SetMode(int flags) { + EnvRenderer_Legacy = flags & ENV_LEGACY; + EnvRenderer_Minimal = flags & ENV_MINIMAL; + OnContextRecreated(NULL); +} + +int EnvRenderer_CalcFlags(const cc_string* mode) { + if (String_CaselessEqualsConst(mode, "normal")) return 0; + if (String_CaselessEqualsConst(mode, "legacy")) return ENV_LEGACY; + if (String_CaselessEqualsConst(mode, "fast")) return ENV_MINIMAL; + /* backwards compatibility */ + if (String_CaselessEqualsConst(mode, "normalfast")) return ENV_MINIMAL; + if (String_CaselessEqualsConst(mode, "legacyfast")) return ENV_LEGACY | ENV_MINIMAL; + + return -1; +} + + +static void OnTexturePackChanged(void* obj) { + /* TODO: Find better way, really should delete them all here */ + Gfx_DeleteTexture(&skybox_tex); +} +static void OnTerrainAtlasChanged(void* obj) { UpdateBorderTextures(); } +static void OnViewDistanceChanged(void* obj) { UpdateAll(); } + +static void OnEnvVariableChanged(void* obj, int envVar) { + if (envVar == ENV_VAR_EDGE_BLOCK) { + MakeBorderTex(&edges_tex, Env.EdgeBlock); + UpdateMapEdges(); + } else if (envVar == ENV_VAR_SIDES_BLOCK) { + MakeBorderTex(&sides_tex, Env.SidesBlock); + UpdateMapSides(); + } else if (envVar == ENV_VAR_EDGE_HEIGHT || envVar == ENV_VAR_SIDES_OFFSET) { + UpdateMapEdges(); + UpdateMapSides(); + } else if (envVar == ENV_VAR_SUN_COLOR) { + UpdateMapEdges(); + } else if (envVar == ENV_VAR_SHADOW_COLOR) { + UpdateMapSides(); + } else if (envVar == ENV_VAR_SKY_COLOR) { + UpdateSky(); + } else if (envVar == ENV_VAR_FOG_COLOR) { + EnvRenderer_UpdateFog(); + } else if (envVar == ENV_VAR_CLOUDS_COLOR) { + UpdateClouds(); + } else if (envVar == ENV_VAR_CLOUDS_HEIGHT) { + UpdateSky(); + UpdateClouds(); + } else if (envVar == ENV_VAR_SKYBOX_COLOR) { + Gfx_DeleteVb(&skybox_vb); + } +} + + +/*########################################################################################################################* +*--------------------------------------------------EnvRenderer component--------------------------------------------------* +*#########################################################################################################################*/ +static void OnInit(void) { + cc_string renderType; + int flags; + Options_UNSAFE_Get(OPT_RENDER_TYPE, &renderType); + + flags = EnvRenderer_CalcFlags(&renderType); + if (flags == -1) flags = 0; + EnvRenderer_Legacy = flags & ENV_LEGACY; + EnvRenderer_Minimal = flags & ENV_MINIMAL; + + TextureEntry_Register(&clouds_entry); + TextureEntry_Register(&skybox_entry); + TextureEntry_Register(&snow_entry); + TextureEntry_Register(&rain_entry); + + Event_Register_(&TextureEvents.PackChanged, NULL, OnTexturePackChanged); + Event_Register_(&TextureEvents.AtlasChanged, NULL, OnTerrainAtlasChanged); + + Event_Register_(&GfxEvents.ViewDistanceChanged, NULL, OnViewDistanceChanged); + Event_Register_(&WorldEvents.EnvVarChanged, NULL, OnEnvVariableChanged); + Event_Register_(&GfxEvents.ContextLost, NULL, OnContextLost); + Event_Register_(&GfxEvents.ContextRecreated, NULL, OnContextRecreated); + + Game_SetViewDistance(Game_UserViewDistance); +} + +static void OnFree(void) { + OnContextLost(NULL); + Mem_Free(Weather_Heightmap); + Weather_Heightmap = NULL; +} + +static void OnReset(void) { + Gfx_SetFog(false); + DeleteVbs(); + + Mem_Free(Weather_Heightmap); + Weather_Heightmap = NULL; + lastPos = IVec3_MaxValue(); +} + +static void OnNewMapLoaded(void) { OnContextRecreated(NULL); } + +struct IGameComponent EnvRenderer_Component = { + OnInit, /* Init */ + OnFree, /* Free */ + OnReset, /* Reset */ + OnReset, /* OnNewMap */ + OnNewMapLoaded /* OnNewMapLoaded */ +}; diff --git a/src/EnvRenderer.h b/src/EnvRenderer.h new file mode 100644 index 0000000..29eb5ad --- /dev/null +++ b/src/EnvRenderer.h @@ -0,0 +1,47 @@ +#ifndef CC_ENVRENDERER_H +#define CC_ENVRENDERER_H +#include "Core.h" +/* +Renders environment of the map (clouds, sky, fog, map sides/edges, skybox, rain/snow) +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent EnvRenderer_Component; + +#define ENV_MINIMAL 1 +#define ENV_LEGACY 2 + +/* Renders coloured sky plane. */ +void EnvRenderer_RenderSky(void); +/* Renders textured cloud plane. */ +void EnvRenderer_RenderClouds(void); +/* Updates current fog colour and mode. */ +void EnvRenderer_UpdateFog(void); + +/* Renders borders around map and under horizon. */ +void EnvRenderer_RenderMapSides(void); +/* Renders flat horizon surrounding map. */ +void EnvRenderer_RenderMapEdges(void); +/* Renders a skybox around the player. */ +void EnvRenderer_RenderSkybox(void); +/* Whether a skybox should be rendered. */ +cc_bool EnvRenderer_ShouldRenderSkybox(void); + +extern cc_int16* Weather_Heightmap; +/* Called when a block is changed to update internal weather state. */ +void EnvRenderer_OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock); +/* Renders rainfall/snowfall weather. */ +void EnvRenderer_RenderWeather(float delta); + +/* Whether large quads are broken down into smaller quads. */ +/* This makes them have less rendering issues when using vertex fog. */ +extern cc_bool EnvRenderer_Legacy; +/* Whether minimal environmental effects are rendered. */ +/* Minimal mode disables skybox, clouds and fog. */ +extern cc_bool EnvRenderer_Minimal; +/* Sets whether Legacy and Minimal modes are used based on given flags. */ +void EnvRenderer_SetMode(int flags); +/* Calculates mode flags for the given mode. */ +/* mode can be: normal, normalfast, legacy, legacyfast */ +CC_NOINLINE int EnvRenderer_CalcFlags(const cc_string* mode); +#endif diff --git a/src/Errors.h b/src/Errors.h new file mode 100644 index 0000000..b1fe9c0 --- /dev/null +++ b/src/Errors.h @@ -0,0 +1,141 @@ +#ifndef CC_ERRORS_H +#define CC_ERRORS_H +/* +Provides a list list of internal ClassiCube errors +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +/* NOTE: When adding errors, remember to keep Logger.c up to date! */ +enum CC_ERRORS { + ERROR_BASE = 0xCCDED000UL, + ERR_END_OF_STREAM = 0xCCDED001UL, /* Attempted to read more data than the stream provided */ + ERR_NOT_SUPPORTED = 0xCCDED002UL, /* Operation is not supported in current state or at all */ + ERR_INVALID_ARGUMENT = 0xCCDED003UL, /* Invalid argument provided to a function */ + ERR_OUT_OF_MEMORY = 0xCCDED004UL, /* Insufficient memory left to perform the requested allocation */ + + OGG_ERR_INVALID_SIG = 0xCCDED005UL, /* Bytes #1-#4 aren't "OggS" */ + OGG_ERR_VERSION = 0xCCDED006UL, /* Byte #5 isn't 0 */ + + WAV_ERR_STREAM_HDR = 0xCCDED007UL, /* Bytes #1-#4 aren't "RIFF" */ + WAV_ERR_STREAM_TYPE = 0xCCDED008UL, /* Bytes #9-#12 aren't "WAV " */ + WAV_ERR_DATA_TYPE = 0xCCDED009UL, /* Audio data type isn't 1 (PCM) */ + AUDIO_ERR_MP3_SIG = 0xCCDED00AUL, /* Signature bytes are "ID3" */ + WAV_ERR_SAMPLE_BITS = 0xCCDED00BUL, /* Bits per sample isn't 16 */ + + SFD_ERR_NEED_DEFAULT_NAME = 0xCCDED00CUL, + + VORBIS_ERR_WRONG_HEADER = 0xCCDED00DUL, /* Packet header doesn't match expected type */ + VORBIS_ERR_FRAMING = 0xCCDED00EUL, /* Framing flag doesn't match expected value */ + VORBIS_ERR_VERSION = 0xCCDED00FUL, /* Vorbis version isn't 0 */ + VORBIS_ERR_BLOCKSIZE = 0xCCDED010UL, /* Invalid blocksize in identifier packet */ + VORBIS_ERR_CHANS = 0xCCDED011UL, /* Either 0 or too many audio channels */ + VORBIS_ERR_TIME_TYPE = 0xCCDED012UL, /* Time object has invalid type */ + VORBIS_ERR_FLOOR_TYPE = 0xCCDED013UL, /* Floor object has invalid type */ + VORBIS_ERR_RESIDUE_TYPE = 0xCCDED014UL, /* Residue object has invalid type */ + VORBIS_ERR_MAPPING_TYPE = 0xCCDED015UL, /* Mapping object has invalid type */ + VORBIS_ERR_MODE_TYPE = 0xCCDED016UL, /* Mode object has invalid type */ + VORBIS_ERR_CODEBOOK_SYNC = 0xCCDED017UL, /* Codebook sync value doesn't match 0x564342 */ + VORBIS_ERR_CODEBOOK_ENTRY = 0xCCDED018UL, /* Codebook specifies an entry outside its size */ + VORBIS_ERR_CODEBOOK_LOOKUP = 0xCCDED019UL, /* Codebook has invalid entry lookup method */ + VORBIS_ERR_MODE_WINDOW = 0xCCDED01AUL, /* Mode object has invalid windowing method */ + VORBIS_ERR_MODE_TRANSFORM = 0xCCDED01BUL, /* Mode object has invalid transform method */ + VORBIS_ERR_MAPPING_CHANS = 0xCCDED01CUL, /* Mapping object has invalid magnitude/angle combination */ + VORBIS_ERR_MAPPING_RESERVED = 0xCCDED01DUL, /* Mapping object has invalid reserved value */ + VORBIS_ERR_FRAME_TYPE = 0xCCDED01EUL, /* Audio packet frametype isn't 0 */ + + PNG_ERR_INVALID_SIG = 0xCCDED01FUL, /* Stream doesn't start with PNG signature */ + PNG_ERR_INVALID_HDR_SIZE = 0xCCDED020UL, /* Header chunk has invalid size */ + PNG_ERR_TOO_WIDE = 0xCCDED021UL, /* Image is over 32,768 pixels wide */ + PNG_ERR_TOO_TALL = 0xCCDED022UL, /* Image is over 32,768 pixels tall */ + PNG_ERR_INVALID_COL_BPP = 0xCCDED023UL, /* Invalid colorspace and bits per sample combination */ + PNG_ERR_COMP_METHOD = 0xCCDED024UL, /* Image uses unsupported compression method */ + PNG_ERR_FILTER = 0xCCDED025UL, /* Image uses unsupported filter method */ + PNG_ERR_INTERLACED = 0xCCDED026UL, /* Image uses interlacing, which is unimplemented */ + PNG_ERR_PAL_SIZE = 0xCCDED027UL, /* Palette chunk has invalid size */ + PNG_ERR_TRANS_COUNT = 0xCCDED028UL, /* Translucent chunk has invalid size */ + PNG_ERR_TRANS_INVALID = 0xCCDED029UL, /* Colorspace doesn't support translucent chunk */ + PNG_ERR_REACHED_IEND = 0xCCDED02AUL, /* Image only has partial data */ + PNG_ERR_NO_DATA = 0xCCDED02BUL, /* Image is missing all data */ + PNG_ERR_INVALID_SCANLINE = 0xCCDED02CUL, /* Image row has invalid type */ + + ZIP_ERR_TOO_MANY_ENTRIES = 0xCCDED02DUL, /* ZIP archive has too many entries */ + ZIP_ERR_SEEK_END_OF_CENTRAL_DIR = 0xCCDED02EUL, /* Failed to seek to end of central directory record */ + ZIP_ERR_NO_END_OF_CENTRAL_DIR = 0xCCDED02FUL, /* Failed to find end of central directory record */ + ZIP_ERR_SEEK_CENTRAL_DIR = 0xCCDED030UL, /* Failed to seek to central directory records */ + ZIP_ERR_INVALID_CENTRAL_DIR = 0xCCDED031UL, /* Central directory record has invalid signature */ + ZIP_ERR_SEEK_LOCAL_DIR = 0xCCDED032UL, /* Failed to seek to a local directory record */ + ZIP_ERR_INVALID_LOCAL_DIR = 0xCCDED033UL, /* Local directory record has invalid signature */ + ZIP_ERR_FILENAME_LEN = 0xCCDED034UL, /* ZIP entry filename is too long */ + + GZIP_ERR_HEADER1 = 0xCCDED035UL, /* GZIP stream byte #1 isn't 0x1F */ + GZIP_ERR_HEADER2 = 0xCCDED036UL, /* GZIP stream byte #2 isn't 0x8B */ + GZIP_ERR_METHOD = 0xCCDED037UL, /* GZIP stream uses unsupported compression method */ + GZIP_ERR_FLAGS = 0xCCDED038UL, /* GZIP stream uses unsupported flags */ + + ZLIB_ERR_METHOD = 0xCCDED039UL, /* ZLIB stream uses unsupported compression method */ + ZLIB_ERR_FLAGS = 0xCCDED03AUL, /* ZLIB stream uses unsupported flags */ + + FCM_ERR_IDENTIFIER = 0xCCDED03BUL, /* FCM stream bytes #1-#4 aren't 0x0FC2AF40 */ + FCM_ERR_REVISION = 0xCCDED03CUL, /* FCM stream byte #5 isn't 13 */ + + LVL_ERR_VERSION = 0xCCDED03DUL, /* LVL stream byte #1-#2 aren't 1874 */ + + DAT_ERR_IDENTIFIER = 0xCCDED03EUL, /* DAT stream bytes #1-#4 aren't 0x271BB788 */ + DAT_ERR_VERSION = 0xCCDED03FUL, /* DAT stream byte #5 isn't 2 */ + DAT_ERR_JIDENTIFIER = 0xCCDED040UL, /* DAT stream bytes #6-#7 aren't 0xACED */ + DAT_ERR_JVERSION = 0xCCDED041UL, /* DAT stream bytes #8-#9 aren't 0x0005 */ + DAT_ERR_ROOT_OBJECT = 0xCCDED042UL, /* DAT version 2 root value isn't an object */ + + JAVA_ERR_INVALID_TYPECODE = 0xCCDED043UL, /* Typecode is invalid or incorrect */ + JAVA_ERR_JSTRING_LEN = 0xCCDED044UL, /* String length is too long */ + JAVA_ERR_JFIELD_CLASS_NAME = 0xCCDED045UL, /* Field classname type is invalid */ + JAVA_ERR_JCLASS_TYPE = 0xCCDED046UL, /* ClassDescriptor type is invalid */ + JAVA_ERR_JCLASS_FIELDS = 0xCCDED047UL, /* ClassDescriptor has too many fields */ + JAVA_ERR_JCLASS_ANNOTATION = 0xCCDED048UL, /* ClassDescriptor uses unsupported annotations */ + JAVA_ERR_JCLASSES_COUNT = 0xCCDED049UL, /* Too many ClassDescriptors in stream */ + JAVA_ERR_JCLASS_REFERENCE = 0xCCDED04AUL, /* Reference refers to non-existent ClassDescriptor */ + JAVA_ERR_JOBJECT_FLAGS = 0xCCDED04BUL, /* Object class isn't deserialisable */ + JAVA_ERR_JVALUE_TYPE = 0xCCDED04CUL, /* Value data type is invalid */ + + SOCK_ERR_UNKNOWN_HOST = 0xCCDED04FUL, /* Host (e.g. "example.com") was unknown to the DNS server(s) */ + + NBT_ERR_UNKNOWN = 0xCCDED050UL, /* NBT tag has an unknown type */ + CW_ERR_ROOT_TAG = 0xCCDED051UL, /* NBT root tag isn't a Compound tag */ + CW_ERR_STRING_LEN = 0xCCDED052UL, /* NBT string is too long */ + CW_ERR_UUID_LEN = 0xCCDED053UL, /* Map UUID byte array length is not 16 */ + + AL_ERR_INIT_DEVICE = 0xCCDED054UL, /* Unknown error occurred creating OpenAL device */ + AL_ERR_INIT_CONTEXT = 0xCCDED055UL, /* Unknown error occurred creating OpenAL context */ + + INF_ERR_BLOCKTYPE = 0xCCDED056UL, /* Block has invalid block type */ + INF_ERR_LEN_VERIFY = 0xCCDED057UL, /* Block length checksum failed */ + INF_ERR_REPEAT_BEG = 0xCCDED058UL, /* Attempted to repeat codewords before first code */ + INF_ERR_REPEAT_END = 0xCCDED059UL, /* Attempted to repeat codewords after last code */ + INF_ERR_INVALID_CODE = 0xCCDED05AUL, /* Attempted to decode unknown codeword */ + INF_ERR_NUM_CODES = 0xCCDED05BUL, /* Too many codewords specified for bit length */ + + ERR_DOWNLOAD_INVALID = 0xCCDED05CUL, /* Unspecified error occurred downloading data */ + ERR_NO_AUDIO_OUTPUT = 0xCCDED05DUL, /* No audio output devices are connected */ + ERR_INVALID_DATA_URL = 0xCCDED05EUL, /* Invalid URL provided to download from */ + ERR_INVALID_OPEN_URL = 0xCCDED05FUL, /* Invalid URL provided to open in new tab */ + + NBT_ERR_EXPECTED_I8 = 0xCCDED060UL, /* Expected I8 NBT tag */ + NBT_ERR_EXPECTED_I16 = 0xCCDED061UL, /* Expected I16 NBT tag */ + NBT_ERR_EXPECTED_I32 = 0xCCDED062UL, /* Expected I32 NBT tag */ + NBT_ERR_EXPECTED_F32 = 0xCCDED063UL, /* Expected F32 NBT tag */ + NBT_ERR_EXPECTED_STR = 0xCCDED064UL, /* Expected String NBT tag */ + NBT_ERR_EXPECTED_ARR = 0xCCDED065UL, /* Expected Byte Array NBT tag */ + NBT_ERR_ARR_TOO_SMALL= 0xCCDED066UL, /* Byte Array NBT tag length is < expected length */ + + HTTP_ERR_NO_SSL = 0xCCDED067UL, /* HTTP backend doesn't support SSL */ + HTTP_ERR_REDIRECTS = 0xCCDED068UL, /* Too many attempted HTTP redirects */ + HTTP_ERR_RELATIVE = 0xCCDED069UL, /* Unsupported relative URL format */ + HTTP_ERR_INVALID_BODY= 0xCCDED06AUL, /* HTTP message doesn't have Content-Length or use Chunked transfer encoding */ + HTTP_ERR_CHUNK_SIZE = 0xCCDED06BUL, /* HTTP message chunk has negative size/length */ + HTTP_ERR_TRUNCATED = 0xCCDED06CUL, /* HTTP response header was truncated due to being too long */ + HTTP_ERR_NO_RESPONSE = 0xCCDED06DUL, /* First attempt to read response returned 0 bytes */ + + SSL_ERR_CONTEXT_DEAD = 0xCCDED070UL, /* Server shutdown the SSL context and it must be recreated */ + PNG_ERR_16BITSAMPLES = 0xCCDED071UL, /* Image uses 16 bit samples, which is unimplemented */ +}; +#endif diff --git a/src/Event.c b/src/Event.c new file mode 100644 index 0000000..1c0d976 --- /dev/null +++ b/src/Event.c @@ -0,0 +1,201 @@ +#include "Event.h" +#include "Logger.h" + +int EventAPIVersion = 3; +struct _EntityEventsList EntityEvents; +struct _TabListEventsList TabListEvents; +struct _TextureEventsList TextureEvents; +struct _GfxEventsList GfxEvents; +struct _UserEventsList UserEvents; +struct _BlockEventsList BlockEvents; +struct _WorldEventsList WorldEvents; +struct _ChatEventsList ChatEvents; +struct _WindowEventsList WindowEvents; +struct _InputEventsList InputEvents; +struct _PointerEventsList PointerEvents; +struct _ControllerEventsList ControllerEvents; +struct _NetEventsList NetEvents; + +void Event_Register(struct Event_Void* handlers, void* obj, Event_Void_Callback handler) { + int i; + for (i = 0; i < handlers->Count; i++) { + /* Attempting to register the same handler twice is usually caused by a bug */ + if (handlers->Handlers[i] == handler && handlers->Objs[i] == obj) { + Logger_Abort("Attempt to register event handler that was already registered"); + } + } + + if (handlers->Count == EVENT_MAX_CALLBACKS) { + Logger_Abort("Unable to register another event handler"); + } else { + handlers->Handlers[handlers->Count] = handler; + handlers->Objs[handlers->Count] = obj; + handlers->Count++; + } +} + +void Event_Unregister(struct Event_Void* handlers, void* obj, Event_Void_Callback handler) { + int i, j; + for (i = 0; i < handlers->Count; i++) { + if (handlers->Handlers[i] != handler || handlers->Objs[i] != obj) continue; + + /* Remove this handler from the list, by shifting all following handlers left */ + for (j = i; j < handlers->Count - 1; j++) { + handlers->Handlers[j] = handlers->Handlers[j + 1]; + handlers->Objs[j] = handlers->Objs[j + 1]; + } + + handlers->Count--; + handlers->Handlers[handlers->Count] = NULL; + handlers->Objs[handlers->Count] = NULL; + return; + } +} + +void Event_UnregisterAll(void) { + /* NOTE: This MUST be kept in sync with Event.h list of events */ + EntityEvents.Added.Count = 0; + EntityEvents.Removed.Count = 0; + + TabListEvents.Added.Count = 0; + TabListEvents.Changed.Count = 0; + TabListEvents.Removed.Count = 0; + + TextureEvents.AtlasChanged.Count = 0; + TextureEvents.PackChanged.Count = 0; + TextureEvents.FileChanged.Count = 0; + + GfxEvents.ViewDistanceChanged.Count = 0; + GfxEvents.LowVRAMDetected.Count = 0; + GfxEvents.ProjectionChanged.Count = 0; + GfxEvents.ContextLost.Count = 0; + GfxEvents.ContextRecreated.Count = 0; + + UserEvents.BlockChanged.Count = 0; + UserEvents.HackPermsChanged.Count = 0; + UserEvents.HeldBlockChanged.Count = 0; + + BlockEvents.PermissionsChanged.Count = 0; + BlockEvents.BlockDefChanged.Count = 0; + + WorldEvents.NewMap.Count = 0; + WorldEvents.Loading.Count = 0; + WorldEvents.MapLoaded.Count = 0; + WorldEvents.EnvVarChanged.Count = 0; + WorldEvents.LightingModeChanged.Count = 0; + + ChatEvents.FontChanged.Count = 0; + ChatEvents.ChatReceived.Count = 0; + ChatEvents.ChatSending.Count = 0; + ChatEvents.ColCodeChanged.Count = 0; + + WindowEvents.RedrawNeeded.Count = 0; + WindowEvents.Resized.Count = 0; + WindowEvents.Closing.Count = 0; + WindowEvents.FocusChanged.Count = 0; + WindowEvents.StateChanged.Count = 0; + WindowEvents.Created.Count = 0; + WindowEvents.InactiveChanged.Count = 0; + WindowEvents.Redrawing.Count = 0; + + InputEvents.Press.Count = 0; + InputEvents.Down.Count = 0; + InputEvents.Up.Count = 0; + InputEvents.Wheel.Count = 0; + InputEvents.TextChanged.Count = 0; + + PointerEvents.Moved.Count = 0; + PointerEvents.Down.Count = 0; + PointerEvents.Up.Count = 0; + PointerEvents.RawMoved.Count = 0; + + ControllerEvents.AxisUpdate.Count = 0; + + NetEvents.Connected.Count = 0; + NetEvents.Disconnected.Count = 0; + NetEvents.PluginMessageReceived.Count = 0; +} + +void Event_RaiseVoid(struct Event_Void* handlers) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i]); + } +} + +void Event_RaiseInt(struct Event_Int* handlers, int arg) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], arg); + } +} + +void Event_RaiseFloat(struct Event_Float* handlers, float arg) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], arg); + } +} + +void Event_RaiseEntry(struct Event_Entry* handlers, struct Stream* stream, const cc_string* name) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], stream, name); + } +} + +void Event_RaiseBlock(struct Event_Block* handlers, IVec3 coords, BlockID oldBlock, BlockID block) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], coords, oldBlock, block); + } +} + +void Event_RaiseChat(struct Event_Chat* handlers, const cc_string* msg, int msgType) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], msg, msgType); + } +} + +void Event_RaiseInput(struct Event_Input* handlers, int key, cc_bool repeating) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], key, repeating); + } +} + +void Event_RaiseString(struct Event_String* handlers, const cc_string* str) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], str); + } +} + +void Event_RaiseRawMove(struct Event_RawMove* handlers, float xDelta, float yDelta) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], xDelta, yDelta); + } +} + +void Event_RaisePadAxis(struct Event_PadAxis* handlers, int port, int axis, float x, float y) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], port, axis, x, y); + } +} + +void Event_RaisePluginMessage(struct Event_PluginMessage* handlers, cc_uint8 channel, cc_uint8* data) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], channel, data); + } +} + +void Event_RaiseLightingMode(struct Event_LightingMode* handlers, cc_uint8 oldMode, cc_bool fromServer) { + int i; + for (i = 0; i < handlers->Count; i++) { + handlers->Handlers[i](handlers->Objs[i], oldMode, fromServer); + } +} \ No newline at end of file diff --git a/src/Event.h b/src/Event.h new file mode 100644 index 0000000..f33ab4b --- /dev/null +++ b/src/Event.h @@ -0,0 +1,222 @@ +#ifndef CC_EVENT_H +#define CC_EVENT_H +#include "Vectors.h" +/* Helper methods for using events, and contains all events. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +/* Max callbacks that can be registered for an event. */ +#define EVENT_MAX_CALLBACKS 32 +struct Stream; + +typedef void (*Event_Void_Callback)(void* obj); +struct Event_Void { + Event_Void_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_Int_Callback)(void* obj, int argument); +struct Event_Int { + Event_Int_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_Float_Callback)(void* obj, float argument); +struct Event_Float { + Event_Float_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_Entry_Callback)(void* obj, struct Stream* stream, const cc_string* name); +struct Event_Entry { + Event_Entry_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_Block_Callback)(void* obj, IVec3 coords, BlockID oldBlock, BlockID block); +struct Event_Block { + Event_Block_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_Chat_Callback)(void* obj, const cc_string* msg, int msgType); +struct Event_Chat { + Event_Chat_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_Input_Callback)(void* obj, int key, cc_bool repeating); +struct Event_Input { + Event_Input_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_String_Callback)(void* obj, const cc_string* str); +struct Event_String { + Event_String_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_RawMove_Callback)(void* obj, float xDelta, float yDelta); +struct Event_RawMove { + Event_RawMove_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_PadAxis_Callback)(void* obj, int port, int axis, float x, float y); +struct Event_PadAxis { + Event_PadAxis_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +/* "data" will be 64 bytes in length. */ +typedef void (*Event_PluginMessage_Callback)(void* obj, cc_uint8 channel, cc_uint8* data); +struct Event_PluginMessage { + Event_PluginMessage_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +typedef void (*Event_LightingMode_Callback)(void* obj, cc_uint8 oldMode, cc_bool fromServer); +struct Event_LightingMode { + Event_LightingMode_Callback Handlers[EVENT_MAX_CALLBACKS]; + void* Objs[EVENT_MAX_CALLBACKS]; int Count; +}; + +/* Registers a callback function for the given event. */ +/* NOTE: Trying to register a callback twice or over EVENT_MAX_CALLBACKS callbacks will terminate the game. */ +CC_API void Event_Register(struct Event_Void* handlers, void* obj, Event_Void_Callback handler); +/* Unregisters a callback function for the given event. */ +CC_API void Event_Unregister(struct Event_Void* handlers, void* obj, Event_Void_Callback handler); +#define Event_Register_(handlers, obj, handler) Event_Register((struct Event_Void*)(handlers), obj, (Event_Void_Callback)(handler)) +#define Event_Unregister_(handlers, obj, handler) Event_Unregister((struct Event_Void*)(handlers), obj, (Event_Void_Callback)(handler)) + +/* Calls all registered callback for an event with no arguments. */ +CC_API void Event_RaiseVoid(struct Event_Void* handlers); +/* Calls all registered callback for an event which has an int argument. */ +/* NOTE: The actual argument "type" may be char, Key, cc_uint8 etc */ +CC_API void Event_RaiseInt(struct Event_Int* handlers, int arg); +/* Calls all registered callbacks for an event which has a float argument. */ +CC_API void Event_RaiseFloat(struct Event_Float* handlers, float arg); + +/* Calls all registered callbacks for an event which has data stream and name arguments. */ +void Event_RaiseEntry(struct Event_Entry* handlers, struct Stream* stream, const cc_string* name); +/* Calls all registered callbacks for an event which takes block change arguments. */ +/* These are the coordinates/location of the change, block there before, block there now. */ +void Event_RaiseBlock(struct Event_Block* handlers, IVec3 coords, BlockID oldBlock, BlockID block); +/* Calls all registered callbacks for an event which has chat message type and contents. */ +/* See MsgType enum in Chat.h for what types of messages there are. */ +void Event_RaiseChat(struct Event_Chat* handlers, const cc_string* msg, int msgType); +/* Calls all registered callbacks for an event which has keyboard key/mouse button. */ +/* repeating is whether the key/button was already pressed down. (i.e. user is holding down key) */ +void Event_RaiseInput(struct Event_Input* handlers, int key, cc_bool repeating); +/* Calls all registered callbacks for an event which has a string argument. */ +void Event_RaiseString(struct Event_String* handlers, const cc_string* str); +/* Calls all registered callbacks for an event which has raw pointer movement arguments. */ +void Event_RaiseRawMove(struct Event_RawMove* handlers, float xDelta, float yDelta); +/* Calls all registered callbacks for an event which has pad axis arguments. */ +void Event_RaisePadAxis(struct Event_PadAxis* handlers, int port, int axis, float x, float y); +/* Calls all registered callbacks for an event which has a channel and a 64 byte data argument. */ +void Event_RaisePluginMessage(struct Event_PluginMessage* handlers, cc_uint8 channel, cc_uint8* data); +/* Calls all registered callbacks for an event called when the Lighting_LightingMode is changed */ +void Event_RaiseLightingMode(struct Event_LightingMode* handlers, cc_uint8 oldMode, cc_bool fromServer); + +void Event_UnregisterAll(void); +/* NOTE: Event_UnregisterAll MUST be updated when events lists are changed */ + +/* Event API version supported by the client */ +/* Version 1 - Added NetEvents.PluginMessageReceived */ +/* Version 2 - Added WindowEvents.Redrawing */ +/* Version 3 - Changed InputEvent.Press from code page 437 to unicode character */ +/* You MUST CHECK the event API version before attempting to use the events listed above, */ +/* as otherwise if the player is using an older client that lacks some of the above events, */ +/* you will be calling Event_Register on random data instead of the expected EventsList struct */ +CC_VAR extern int EventAPIVersion; + +CC_VAR extern struct _EntityEventsList { + struct Event_Int Added; /* Entity is spawned in the current world */ + struct Event_Int Removed; /* Entity is despawned from the current world */ +} EntityEvents; + +CC_VAR extern struct _TabListEventsList { + struct Event_Int Added; /* Tab list entry is created */ + struct Event_Int Changed; /* Tab list entry is modified */ + struct Event_Int Removed; /* Tab list entry is removed */ +} TabListEvents; + +CC_VAR extern struct _TextureEventsList { + struct Event_Void AtlasChanged; /* Terrain atlas (terrain.png) is changed */ + struct Event_Void PackChanged; /* Texture pack is changed */ + struct Event_Entry FileChanged; /* File in a texture pack is changed (terrain.png, rain.png) */ +} TextureEvents; + +CC_VAR extern struct _GfxEventsList { + struct Event_Void ViewDistanceChanged; /* View/fog distance is changed */ + struct Event_Void LowVRAMDetected; /* Insufficient VRAM detected, need to free some GPU resources */ + struct Event_Void ProjectionChanged; /* Projection matrix has changed */ + struct Event_Void ContextLost; /* Context is destroyed after having been previously created */ + struct Event_Void ContextRecreated; /* Context is recreated after having been previously lost */ +} GfxEvents; + +CC_VAR extern struct _UserEventsList { + struct Event_Block BlockChanged; /* User changes a block */ + struct Event_Void HackPermsChanged; /* Hack permissions of the player changes */ + struct Event_Void HeldBlockChanged; /* Held block in hotbar changes */ + struct Event_Void HacksStateChanged; /* Hack states changed (e.g. stops flying) */ +} UserEvents; + +CC_VAR extern struct _BlockEventsList { + struct Event_Void PermissionsChanged; /* Block permissions (can place/delete) for a block changes */ + struct Event_Void BlockDefChanged; /* Block definition is changed or removed */ +} BlockEvents; + +CC_VAR extern struct _WorldEventsList { + struct Event_Void NewMap; /* Player begins loading a new world */ + struct Event_Float Loading; /* Portion of world is decompressed/generated (Arg is progress from 0-1) */ + struct Event_Void MapLoaded; /* New world has finished loading, player can now interact with it */ + struct Event_Int EnvVarChanged; /* World environment variable changed by player/CPE/WoM config */ + struct Event_LightingMode LightingModeChanged; /* Lighting mode changed. */ +} WorldEvents; + +CC_VAR extern struct _ChatEventsList { + struct Event_Void FontChanged; /* User changes whether system chat font used, and when the bitmapped font texture changes */ + struct Event_Chat ChatReceived; /* Raised when message is being added to chat */ + struct Event_Chat ChatSending; /* Raised when user sends a message */ + struct Event_Int ColCodeChanged; /* Raised when a colour code changes */ +} ChatEvents; + +CC_VAR extern struct _WindowEventsList { + struct Event_Void RedrawNeeded; /* Window contents invalidated and will need to be redrawn */ + struct Event_Void Resized; /* Window is resized */ + struct Event_Void Closing; /* Window is about to close (should free resources/save state/etc here) */ + struct Event_Void FocusChanged; /* Focus of the window changed */ + struct Event_Void StateChanged; /* State of the window changed (e.g. minimised, fullscreen) */ + struct Event_Void Created; /* Window has been created, Window_Handle is valid now. */ + struct Event_Void InactiveChanged; /* Inactive/background state of the window changed */ + struct Event_Void Redrawing; /* Window contents should be redrawn (as they are about to be displayed) */ +} WindowEvents; + +CC_VAR extern struct _InputEventsList { + struct Event_Int Press; /* Key input character is typed. Arg is a unicode character */ + struct Event_Input Down; /* Key or button is pressed. Arg is a member of Key enumeration */ + struct Event_Int Up; /* Key or button is released. Arg is a member of Key enumeration */ + struct Event_Float Wheel; /* Mouse wheel is moved/scrolled (Arg is wheel delta) */ + struct Event_String TextChanged; /* Text in the on-screen input keyboard changed (for Mobile) */ +} InputEvents; + +CC_VAR extern struct _PointerEventsList { + struct Event_Int Moved; /* Pointer position changed (Arg is index) */ + struct Event_Int Down; /* Left mouse or touch is pressed (Arg is index) */ + struct Event_Int Up; /* Left mouse or touch is released (Arg is index) */ + struct Event_RawMove RawMoved; /* Raw pointer position changed (Arg is delta) */ +} PointerEvents; + +CC_VAR extern struct _ControllerEventsList { + struct Event_PadAxis AxisUpdate; /* Raw analog controller movement */ +} ControllerEvents; + +CC_VAR extern struct _NetEventsList { + struct Event_Void Connected; /* Connection to a server was established */ + struct Event_Void Disconnected; /* Connection to the server was lost */ + struct Event_PluginMessage PluginMessageReceived; /* Received a PluginMessage packet from the server */ +} NetEvents; +#endif diff --git a/src/ExtMath.c b/src/ExtMath.c new file mode 100644 index 0000000..c1409c7 --- /dev/null +++ b/src/ExtMath.c @@ -0,0 +1,468 @@ +#include "ExtMath.h" +#include "Platform.h" +#include "Utils.h" +/* For abs(x) function */ +#include + +#define PI 3.141592653589793238462643383279502884197169399 + +/* Sega saturn is missing these intrinsics */ +#ifdef CC_BUILD_SATURN +#include +extern int32_t fix16_sqrt(int32_t value); +static int abs(int x) { return x < 0 ? -x : x; } + +float sqrtf(float x) { + int32_t fp_x = (int32_t)(x * (1 << 16)); + fp_x = fix16_sqrt(fp_x); + return (float)fp_x / (1 << 16); + } +#endif + + +#if defined CC_BUILD_PS1 + /* PS1 is missing these intrinsics */ + #include + float Math_AbsF(float x) { return __builtin_fabsf(x); } + + float Math_SqrtF(float x) { + int fp_x = (int)(x * (1 << 12)); + fp_x = SquareRoot12(fp_x); + return (float)fp_x / (1 << 12); + } +#elif defined __GNUC__ + /* Defined in .h using builtins */ +#else + #include + + float Math_AbsF(float x) { return fabsf(x); /* MSVC intrinsic */ } + float Math_SqrtF(float x) { return sqrtf(x); /* MSVC intrinsic */ } +#endif + +float Math_Mod1(float x) { return x - (int)x; /* fmodf(x, 1); */ } +int Math_AbsI(int x) { return abs(x); /* MSVC intrinsic */ } + +int Math_Floor(float value) { + int valueI = (int)value; + return valueI > value ? valueI - 1 : valueI; +} + +int Math_Ceil(float value) { + int valueI = (int)value; + return valueI < value ? valueI + 1 : valueI; +} + +int Math_ilog2(cc_uint32 value) { + cc_uint32 r = 0; + while (value >>= 1) r++; + return r; +} + +int Math_CeilDiv(int a, int b) { + return a / b + (a % b != 0 ? 1 : 0); +} + +int Math_Sign(float value) { + if (value > 0.0f) return +1; + if (value < 0.0f) return -1; + return 0; +} + +float Math_Lerp(float a, float b, float t) { + return a + (b - a) * t; +} + +float Math_ClampAngle(float degrees) { + while (degrees >= 360.0f) degrees -= 360.0f; + while (degrees < 0.0f) degrees += 360.0f; + return degrees; +} + +float Math_LerpAngle(float leftAngle, float rightAngle, float t) { + /* Need to potentially adjust a bit when interpolating some angles */ + /* Consider 350* --> 0*, we only want to interpolate across the 10* */ + /* But without adjusting for this case, we would interpolate back the whole 350* degrees */ + cc_bool invertLeft = leftAngle > 270.0f && rightAngle < 90.0f; + cc_bool invertRight = rightAngle > 270.0f && leftAngle < 90.0f; + if (invertLeft) leftAngle = leftAngle - 360.0f; + if (invertRight) rightAngle = rightAngle - 360.0f; + + return Math_Lerp(leftAngle, rightAngle, t); +} + +int Math_NextPowOf2(int value) { + int next = 1; + while (value > next) { next <<= 1; } + return next; +} + +cc_bool Math_IsPowOf2(int value) { + return value != 0 && (value & (value - 1)) == 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------Random number generator------------------------------------------------* +*#########################################################################################################################*/ +#define RND_VALUE (0x5DEECE66DULL) +#define RND_MASK ((1ULL << 48) - 1) + +void Random_SeedFromCurrentTime(RNGState* rnd) { + cc_uint64 now = Stopwatch_Measure(); + Random_Seed(rnd, (int)now); +} + +void Random_Seed(RNGState* seed, int seedInit) { + *seed = (seedInit ^ RND_VALUE) & RND_MASK; +} + +int Random_Next(RNGState* seed, int n) { + cc_int64 raw; + int bits, val; + + if ((n & -n) == n) { /* i.e., n is a power of 2 */ + *seed = (*seed * RND_VALUE + 0xBLL) & RND_MASK; + raw = (cc_int64)(*seed >> (48 - 31)); + return (int)((n * raw) >> 31); + } + + do { + *seed = (*seed * RND_VALUE + 0xBLL) & RND_MASK; + bits = (int)(*seed >> (48 - 31)); + val = bits % n; + } while (bits - val + (n - 1) < 0); + return val; +} + +float Random_Float(RNGState* seed) { + int raw; + + *seed = (*seed * RND_VALUE + 0xBLL) & RND_MASK; + raw = (int)(*seed >> (48 - 24)); + return raw / ((float)(1 << 24)); +} + + +/*########################################################################################################################* +*--------------------------------------------------Transcendental functions-----------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_DREAMCAST +#include + +/* If don't have some code referencing libm, then gldc will fail to link with undefined reference to fabs */ +/* TODO: Properly investigate this issue */ +/* double make_dreamcast_build_compile(void) { fabs(4); } */ + +float Math_SinF(float x) { return sinf(x); } +float Math_CosF(float x) { return cosf(x); } +double Math_Exp2(double x) { return exp2(x); } +double Math_Log2(double x) { return log2(x); } +#else +/***** Caleb's Math functions *****/ + +/* This code implements the math functions sine, cosine, arctangent, the + * exponential function, and the logarithmic function. The code uses techniques + * exclusively described in the book "Computer Approximations" by John Fraser + * Hart (1st Edition). Each function approximates their associated math function + * the same way: + * + * 1. First, the function uses properties of the associated math function to + * reduce the input range to a small finite interval, + * + * 2. Second, the function calculates a polynomial, rational, or similar + * function that approximates the associated math function on that small + * finite interval to the desired accuracy. These polynomial, rational, or + * similar functions were calculated by the authors of "Computer + * Approximations" using the Remez algorithm and exist in the book's + * appendix. + */ + +/* NOTE: NaN/Infinity checking was removed from Cos/Sin functions, */ +/* since ClassiCube does not care about the exact return value */ +/* from the mathematical functions anyways */ + +/* Global constants */ +static const double SQRT2 = 1.4142135623730950488016887242096980785696718753769; +#define DIV_2_PI (1.0 / (2.0 * PI)) + +static const cc_uint64 _DBL_NAN = 0x7FF8000000000000ULL; +#define DBL_NAN *((double*)&_DBL_NAN) +static const cc_uint64 _POS_INF = 0x7FF0000000000000ULL; +#define POS_INF *((double*)&_POS_INF) +static const cc_uint64 _NEG_INF = 0xFFF0000000000000ULL; +#define NEG_INF *((double*)&_NEG_INF) + +/* Calculates the floor of a double. + */ +static double Floord(double x) { + if (x >= 0) + return (double) ((int) x); + return (double) (((int) x) - 1); +} + +/************ + * Math_Sin * + ************/ + +/* Calculates the 5th degree polynomial function SIN 2922 listed in the book's + * appendix. + * + * Associated math function: sin(pi/6 * x) + * Allowed input range: [0, 1] + * Precision: 16.47 + */ +static double SinStage1(double x) { + const static double A[] = { + .52359877559829885532, + -.2392459620393377657e-1, + .32795319441392666e-3, + -.214071970654441e-5, + .815113605169e-8, + -.2020852964e-10, + }; + + double P = A[5]; + double x_2 = x * x; + int i; + + for (i = 4; i >= 0; i--) { + P *= x_2; + P += A[i]; + } + P *= x; + return P; +} + +/* Uses the property + * sin(x) = sin(x/3) * (3 - 4 * (sin(x/3))^2) + * to reduce the input range of sin(x) to [0, pi/6]. + * + * Associated math function: sin(2 * pi * x) + * Allowed input range: [0, 0.25] + */ +static double SinStage2(double x) { + double sin_6 = SinStage1(x * 4.0); + return sin_6 * (3.0 - 4.0 * sin_6 * sin_6); +} + +/* Uses the properties of sine to reduce the input range from [0, 2*pi] to [0, + * pi/2]. + * + * Associated math function: sin(2 * pi * x) + * Allowed input range: [0, 1] + */ +static double SinStage3(double x) { + if (x < 0.25) + return SinStage2(x); + if (x < 0.5) + return SinStage2(0.5 - x); + if (x < 0.75) + return -SinStage2(x - 0.5); + return -SinStage2(1.0 - x); +} + +/* Since sine has a period of 2*pi, this function maps any real number to a + * number from [0, 2*pi]. + * + * Associated math function: sin(x) + * Allowed input range: anything + */ +float Math_SinF(float x) { + double x_div_pi; + + x_div_pi = x * DIV_2_PI; + return (float)SinStage3(x_div_pi - Floord(x_div_pi)); +} + +/************ + * Math_Cos * + ************/ + +/* This function works just like the above sine function, except it shifts the + * input by pi/2, using the property cos(x) = sin(x + pi/2). + * + * Associated math function: cos(x) + * Allowed input range: anything + */ +float Math_CosF(float x) { + double x_div_pi_shifted; + + x_div_pi_shifted = x * DIV_2_PI + 0.25; + return (float)SinStage3(x_div_pi_shifted - Floord(x_div_pi_shifted)); +} + +/************ + * Math_Exp * + ************/ + +/* Calculates the function EXPB 1067 listed in the book's appendix. It is of the + * form + * (Q(x^2) + x*P(x^2)) / (Q(x^2) - x*P(x^2)) + * + * Associated math function: 2^x + * Allowed input range: [-1/2, 1/2] + * Precision: 18.08 + */ +static double Exp2Stage1(double x) { + const double A_P[] = { + .1513906799054338915894328e4, + .20202065651286927227886e2, + .23093347753750233624e-1, + }; + + const double A_Q[] = { + .4368211662727558498496814e4, + .233184211427481623790295e3, + 1.0, + }; + + double x_2 = x * x; + double P, Q; + int i; + + P = A_P[2]; + for (i = 1; i >= 0; i--) { + P *= x_2; + P += A_P[i]; + } + P *= x; + + Q = A_Q[2]; + for (i = 1; i >= 0; i--) { + Q *= x_2; + Q += A_Q[i]; + } + + return (Q + P) / (Q - P); +} + +/* Reduces the range of 2^x to [-1/2, 1/2] by using the property + * 2^x = 2^(integer value) * 2^(fractional part). + * 2^(integer value) can be calculated by directly manipulating the bits of the + * double-precision floating point representation. + * + * Associated math function: 2^x + * Allowed input range: anything + */ +double Math_Exp2(double x) { + int x_int; + union { double d; cc_uint64 i; } doi; + + if (x == POS_INF || x == DBL_NAN) + return x; + if (x == NEG_INF) + return 0.0; + + x_int = (int) x; + + if (x < 0) + x_int--; + + if (x_int < -1022) + return 0.0; + if (x_int > 1023) + return POS_INF; + + doi.i = x_int + 1023; + doi.i <<= 52; + + return doi.d * SQRT2 * Exp2Stage1(x - (double) x_int - 0.5); +} + +/************ + * Math_Log * + ************/ + +/* Calculates the 3rd/3rd degree rational function LOG2 2524 listed in the + * book's appendix. + * + * Associated math function: log_2(x) + * Allowed input range: [0.5, 1] + * Precision: 8.32 + */ +static double Log2Stage1(double x) { + const double A_P[] = { + -.205466671951e1, + -.88626599391e1, + .610585199015e1, + .481147460989e1, + }; + + const double A_Q[] = { + .353553425277, + .454517087629e1, + .642784209029e1, + 1.0, + }; + + double P, Q; + int i; + + P = A_P[3]; + for (i = 2; i >= 0; i--) { + P *= x; + P += A_P[i]; + } + + Q = A_Q[3]; + for (i = 2; i >= 0; i--) { + Q *= x; + Q += A_Q[i]; + } + + return P / Q; +} + +/* Reduces the range of log_2(x) by using the property that + * log_2(x) = (x's exponent part) + log_2(x's mantissa part) + * So, by manipulating the bits of the double-precision floating point number + * one can reduce the range of the logarithm function. + * + * Associated math function: log_2(x) + * Allowed input range: anything + */ +double Math_Log2(double x) { + union { double d; cc_uint64 i; } doi; + int exponent; + + if (x == POS_INF) + return POS_INF; + + if (x == DBL_NAN || x <= 0.0) + return DBL_NAN; + + doi.d = x; + exponent = (doi.i >> 52); + exponent -= 1023; + + doi.i |= (((cc_uint64) 1023) << 52); + doi.i &= ~(((cc_uint64) 1024) << 52); + + return exponent + Log2Stage1(doi.d); +} +#endif + +// Approximation of atan2f using the Remez algorithm +// https://math.stackexchange.com/a/1105038 +float Math_Atan2f(float x, float y) { + if (x == 0) { + if (y > 0) return PI / 2.0f; + if (y < 0) return -PI / 2.0f; + return 0; /* Should probably be NaN */ + } + + float ax = Math_AbsF(x); + float ay = Math_AbsF(y); + + float a = (ax < ay) ? (ax / ay) : (ay / ax); + float s = a * a; + float r = ((-0.0464964749f * s + 0.15931422f) * s - 0.327622764f) * s * a + a; + + if (ay > ax) r = 1.57079637f - r; + if (x < 0) r = 3.14159274f - r; + if (y < 0) r = -r; + return r; +} + +double Math_Sin(double x) { return Math_SinF(x); } +double Math_Cos(double x) { return Math_CosF(x); } \ No newline at end of file diff --git a/src/ExtMath.h b/src/ExtMath.h new file mode 100644 index 0000000..b05289d --- /dev/null +++ b/src/ExtMath.h @@ -0,0 +1,78 @@ +#ifndef CC_MATH_H +#define CC_MATH_H +#include "Core.h" +/* Simple math functions and constants. Also implements a RNG algorithm, based on + Java's implementation from https://docs.oracle.com/javase/7/docs/api/java/util/Random.html + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +#define MATH_PI 3.1415926535897931f +#define MATH_DEG2RAD (MATH_PI / 180.0f) +#define MATH_RAD2DEG (180.0f / MATH_PI) +#define MATH_LARGENUM 1000000000.0f + +#define Math_Deg2Packed(x) ((cc_uint8)((x) * 256.0f / 360.0f)) +#define Math_Packed2Deg(x) ((x) * 360.0f / 256.0f) + +#if defined __GNUC__ && !defined CC_PLAT_PS1 +/* fabsf/sqrtf are single intrinsic instructions in gcc/clang */ +/* (sqrtf is only when -fno-math-errno though) */ +#define Math_AbsF(x) __builtin_fabsf(x) +#define Math_SqrtF(x) __builtin_sqrtf(x) +#else +float Math_AbsF(float x); +float Math_SqrtF(float x); +#endif + +float Math_Mod1(float x); +int Math_AbsI(int x); + +CC_API double Math_Sin(double x); +CC_API double Math_Cos(double x); +CC_API float Math_SinF(float x); +CC_API float Math_CosF(float x); +/* Computes atan2(y, x), intended primarily for angle calculation*/ +/* Note that accuracy is only up to around 4 decimal places */ +float Math_Atan2f(float x, float y); + +/* Computes log2(x). Can also be used to approximate log_y(x). */ +/* e.g. for log3(x), use: log2(x)/log2(3) */ +double Math_Log2(double x); +/* Computes 2^x. Can also be used to approximate y^x. */ +/* e.g. for 3^x, use: exp2(log2(3)*x) */ +double Math_Exp2(double x); + +int Math_Floor(float value); +int Math_Ceil(float value); +int Math_ilog2(cc_uint32 value); +int Math_CeilDiv(int a, int b); +int Math_Sign(float value); + +/* Clamps the given angle so it lies between [0, 360) */ +float Math_ClampAngle(float degrees); +/* Linearly interpolates between a and b */ +float Math_Lerp(float a, float b, float t); +/* Linearly interpolates between a given angle range, adjusting if necessary. */ +float Math_LerpAngle(float leftAngle, float rightAngle, float t); + +int Math_NextPowOf2(int value); +cc_bool Math_IsPowOf2(int value); +#define Math_Clamp(val, min, max) val = val < (min) ? (min) : val; val = val > (max) ? (max) : val; + +typedef cc_uint64 RNGState; +/* Initialises RNG using seed from current UTC time. */ +void Random_SeedFromCurrentTime(RNGState* rnd); +/* Initialised RNG using the given seed. */ +CC_API void Random_Seed( RNGState* rnd, int seed); +typedef void (*FP_Random_Seed)(RNGState* rnd, int seed); + +/* Returns integer from 0 inclusive to n exclusive */ +CC_API int Random_Next( RNGState* rnd, int n); +typedef int (*FP_Random_Next)(RNGState* rnd, int n); +/* Returns real from 0 inclusive to 1 exclusive */ +CC_API float Random_Float(RNGState* rnd); +/* Returns integer from min inclusive to max exclusive */ +static CC_INLINE int Random_Range(RNGState* rnd, int min, int max) { + return min + Random_Next(rnd, max - min); +} +#endif diff --git a/src/FancyLighting.c b/src/FancyLighting.c new file mode 100644 index 0000000..d9429fc --- /dev/null +++ b/src/FancyLighting.c @@ -0,0 +1,580 @@ +#include "Lighting.h" +#include "Block.h" +#include "Funcs.h" +#include "MapRenderer.h" +#include "Platform.h" +#include "World.h" +#include "Logger.h" +#include "Event.h" +#include "Game.h" +#include "String.h" +#include "Chat.h" +#include "ExtMath.h" +#include "Options.h" +#include "Queue.h" + + +struct LightNode { + IVec3 coords; /* 12 bytes */ + cc_uint8 brightness; /* 1 byte */ + /* char padding[3]; */ +}; + +static struct Queue lightQueue; +static struct Queue unlightQueue; + +/* Top face, X face, Z face, bottomY face*/ +#define PALETTE_SHADES 4 +/* One palette-group for sunlight, one palette-group for shadow */ +#define PALLETE_GROUP_COUNT 2 +#define PALETTE_COUNT (PALETTE_SHADES * PALLETE_GROUP_COUNT) + +#define PALETTE_YMAX_INDEX 0 +#define PALETTE_XSIDE_INDEX 1 +#define PALETTE_ZSIDE_INDEX 2 +#define PALETTE_YMIN_INDEX 3 + +/* Index into palettes of light colors. */ +/* There are 8 different palettes: Four block-face shades for shadowed areas and four block-face shades for sunlit areas. */ +/* A palette is a 16x16 color array indexed by a byte where the leftmost 4 bits represent lamplight level and the rightmost 4 bits represent lavalight level */ +/* E.G. myPalette[0b_0010_0001] will give us the color for lamp level 2 and lava level 1 (lowest level is 0) */ +static PackedCol* palettes[PALETTE_COUNT]; + +typedef cc_uint8* LightingChunk; +static cc_uint8* chunkLightingDataFlags; +#define CHUNK_UNCALCULATED 0 +#define CHUNK_SELF_CALCULATED 1 +#define CHUNK_ALL_CALCULATED 2 +static LightingChunk* chunkLightingData; + +#define MakePaletteIndex(lampLevel, lavaLevel) ((lampLevel << FANCY_LIGHTING_LAMP_SHIFT) | lavaLevel) +/* Fill in a palette with values based on the current light colors, shaded by the given shade value and lightened by the given ambientColor */ +static void InitPalette(PackedCol* palette, float shaded, PackedCol ambientColor) { + PackedCol lavaColor, lampColor; + int lampLevel, lavaLevel; + float curLerp; + + for (lampLevel = 0; lampLevel < FANCY_LIGHTING_LEVELS; lampLevel++) { + for (lavaLevel = 0; lavaLevel < FANCY_LIGHTING_LEVELS; lavaLevel++) { + if (lampLevel == FANCY_LIGHTING_LEVELS - 1) { + lampColor = Env.LampLightCol; + } + else { + curLerp = lampLevel / (float)(FANCY_LIGHTING_LEVELS - 1); + curLerp *= (MATH_PI / 2); + curLerp = Math_CosF(curLerp); + lampColor = PackedCol_Lerp(0, Env.LampLightCol, 1 - curLerp); + } + + curLerp = lavaLevel / (float)(FANCY_LIGHTING_LEVELS - 1); + curLerp *= (MATH_PI / 2); + curLerp = Math_CosF(curLerp); + + lavaColor = PackedCol_Lerp(0, Env.LavaLightCol, 1 - curLerp); + + /* Blend the two light colors together, then blend that with the ambient color, then shade that by the face darkness */ + palette[MakePaletteIndex(lampLevel, lavaLevel)] = + PackedCol_Scale(PackedCol_ScreenBlend(PackedCol_ScreenBlend(lampColor, lavaColor), ambientColor), shaded); + } + } +} +static void InitPalettes(void) { + int i; + for (i = 0; i < PALETTE_COUNT; i++) { + palettes[i] = (PackedCol*)Mem_Alloc(FANCY_LIGHTING_LEVELS * FANCY_LIGHTING_LEVELS, sizeof(PackedCol), "light color palette"); + } + i = 0; + InitPalette(palettes[i + PALETTE_YMAX_INDEX], 1, Env.ShadowCol); + InitPalette(palettes[i + PALETTE_XSIDE_INDEX], PACKEDCOL_SHADE_X, Env.ShadowCol); + InitPalette(palettes[i + PALETTE_ZSIDE_INDEX], PACKEDCOL_SHADE_Z, Env.ShadowCol); + InitPalette(palettes[i + PALETTE_YMIN_INDEX], PACKEDCOL_SHADE_YMIN, Env.ShadowCol); + i += PALETTE_SHADES; + InitPalette(palettes[i + PALETTE_YMAX_INDEX], 1, Env.SunCol); + InitPalette(palettes[i + PALETTE_XSIDE_INDEX], PACKEDCOL_SHADE_X, Env.SunCol); + InitPalette(palettes[i + PALETTE_ZSIDE_INDEX], PACKEDCOL_SHADE_Z, Env.SunCol); + InitPalette(palettes[i + PALETTE_YMIN_INDEX], PACKEDCOL_SHADE_YMIN, Env.SunCol); +} +static void FreePalettes(void) { + int i; + for (i = 0; i < PALETTE_COUNT; i++) { + Mem_Free(palettes[i]); + } +} + +static int chunksCount; +static void AllocState(void) { + ClassicLighting_AllocState(); + InitPalettes(); + chunksCount = World.ChunksCount; + + chunkLightingDataFlags = (cc_uint8*)Mem_AllocCleared(chunksCount, sizeof(cc_uint8), "light flags"); + chunkLightingData = (LightingChunk*)Mem_AllocCleared(chunksCount, sizeof(LightingChunk), "light chunks"); + Queue_Init(&lightQueue, sizeof(struct LightNode)); + Queue_Init(&unlightQueue, sizeof(struct LightNode)); +} + +static void FreeState(void) { + int i; + ClassicLighting_FreeState(); + + /* This function can be called multiple times without calling AllocState, so... */ + if (!chunkLightingDataFlags) return; + + FreePalettes(); + + for (i = 0; i < chunksCount; i++) { + Mem_Free(chunkLightingData[i]); + } + + Mem_Free(chunkLightingDataFlags); + Mem_Free(chunkLightingData); + chunkLightingDataFlags = NULL; + chunkLightingData = NULL; + Queue_Clear(&lightQueue); + Queue_Clear(&unlightQueue); +} + +/* Converts chunk x/y/z coordinates to the corresponding index in chunks array/list */ +#define ChunkCoordsToIndex(cx, cy, cz) (((cy) * World.ChunksZ + (cz)) * World.ChunksX + (cx)) +/* Converts local x/y/z coordinates to the corresponding index in a chunk */ +#define LocalCoordsToIndex(lx, ly, lz) ((lx) | ((lz) << CHUNK_SHIFT) | ((ly) << (CHUNK_SHIFT * 2))) +/* Converts global x/y/z coordinates to the corresponding index in a chunk */ +#define GlobalCoordsToChunkCoordsIndex(x, y, z) (LocalCoordsToIndex(x & CHUNK_MASK, y & CHUNK_MASK, z & CHUNK_MASK)) + +/* Sets the light level at this cell. Does NOT check that the cell is in bounds. */ +static void SetBrightness(cc_uint8 brightness, int x, int y, int z, cc_bool isLamp, cc_bool refreshChunk) { + cc_uint8 clearMask, shift = isLamp ? FANCY_LIGHTING_LAMP_SHIFT : 0, prevValue; + int cx = x >> CHUNK_SHIFT, lx = x & CHUNK_MASK; + int cy = y >> CHUNK_SHIFT, ly = y & CHUNK_MASK; + int cz = z >> CHUNK_SHIFT, lz = z & CHUNK_MASK; + int chunkIndex = ChunkCoordsToIndex(cx, cy, cz); + int localIndex = LocalCoordsToIndex(lx, ly, lz); + + if (chunkLightingData[chunkIndex] == NULL) { + chunkLightingData[chunkIndex] = (cc_uint8*)Mem_TryAllocCleared(CHUNK_SIZE_3, sizeof(cc_uint8)); + } + + /* 00001111 if lamp, otherwise 11110000*/ + clearMask = ~(FANCY_LIGHTING_MAX_LEVEL << shift); + + if (refreshChunk) { + prevValue = chunkLightingData[chunkIndex][localIndex]; + + chunkLightingData[chunkIndex][localIndex] &= clearMask; + chunkLightingData[chunkIndex][localIndex] |= brightness << shift; + + /* There is no reason to refresh current chunk as the builder does that automatically */ + if (prevValue != chunkLightingData[chunkIndex][localIndex]) { + if (lx == CHUNK_MAX) MapRenderer_RefreshChunk(cx + 1, cy, cz); + if (lx == 0) MapRenderer_RefreshChunk(cx - 1, cy, cz); + if (ly == CHUNK_MAX) MapRenderer_RefreshChunk(cx, cy + 1, cz); + if (ly == 0) MapRenderer_RefreshChunk(cx, cy - 1, cz); + if (lz == CHUNK_MAX) MapRenderer_RefreshChunk(cx, cy, cz + 1); + if (lz == 0) MapRenderer_RefreshChunk(cx, cy, cz - 1); + } + } + else { + chunkLightingData[chunkIndex][localIndex] &= clearMask; + chunkLightingData[chunkIndex][localIndex] |= brightness << shift; + } +} +/* Returns the light level at this cell. Does NOT check that the cell is in bounds. */ +static cc_uint8 GetBrightness(int x, int y, int z, cc_bool isLamp) { + int cx = x >> CHUNK_SHIFT, lx = x & CHUNK_MASK; + int cy = y >> CHUNK_SHIFT, ly = y & CHUNK_MASK; + int cz = z >> CHUNK_SHIFT, lz = z & CHUNK_MASK; + int chunkIndex = ChunkCoordsToIndex(cx, cy, cz), localIndex; + + if (chunkLightingData[chunkIndex] == NULL) { return 0; } + localIndex = LocalCoordsToIndex(lx, ly, lz); + + return isLamp ? + chunkLightingData[chunkIndex][localIndex] >> FANCY_LIGHTING_LAMP_SHIFT : + chunkLightingData[chunkIndex][localIndex] & FANCY_LIGHTING_MAX_LEVEL; +} + + +/* Light can never pass through a block that's full sized and blocks light */ +/* We can assume a block is full sized if none of the LightOffset flags are 0 */ +#define IsFullOpaque(thisBlock) (Blocks.BlocksLight[thisBlock] && Blocks.LightOffset[thisBlock] == 0xFF) + +/* If it's not opaque and it doesn't block light, or it is brighter than 0, we can always pass through */ +/* Light can always pass through leaves and water */ +#define IsFullTransparent(thisBlock)\ +(\ +(Blocks.Draw[thisBlock] > DRAW_OPAQUE && !Blocks.BlocksLight[thisBlock]) || \ +Blocks.Draw[thisBlock] == DRAW_TRANSPARENT_THICK || \ +Blocks.Draw[thisBlock] == DRAW_TRANSLUCENT\ +) + +static cc_bool CanLightPass(BlockID thisBlock, Face face) { + if (IsFullTransparent(thisBlock)) { return true; } + if (Blocks.Brightness[thisBlock]) { return true; } + if (IsFullOpaque(thisBlock)) { return false; } + /* Is stone's face hidden by thisBlock? TODO: Don't hardcode using stone */ + return !Block_IsFaceHidden(BLOCK_STONE, thisBlock, face); +} + +#define Light_TrySpreadInto(axis, AXIS, dir, limit, isLamp, thisFace, thatFace) \ + if (ln.coords.axis dir ## = limit && \ + CanLightPass(thisBlock, FACE_ ## AXIS ## thisFace) && \ + CanLightPass(World_GetBlock(ln.coords.x, ln.coords.y, ln.coords.z), FACE_ ## AXIS ## thatFace) && \ + GetBrightness(ln.coords.x, ln.coords.y, ln.coords.z, isLamp) < ln.brightness) { \ + Queue_Enqueue(&lightQueue, &ln); \ + } \ + +static void FlushLightQueue(cc_bool isLamp, cc_bool refreshChunk) { + struct LightNode ln; + cc_uint8 brightnessHere; + BlockID thisBlock; + + while (lightQueue.count > 0) { + ln = *(struct LightNode*)(Queue_Dequeue(&lightQueue)); + + brightnessHere = GetBrightness(ln.coords.x, ln.coords.y, ln.coords.z, isLamp); + + /* If this cell is already more lit, we can assume this cell and its neighbors have been accounted for */ + if (brightnessHere >= ln.brightness) { continue; } + if (ln.brightness == 0) { continue; } + + SetBrightness(ln.brightness, ln.coords.x, ln.coords.y, ln.coords.z, isLamp, refreshChunk); + + thisBlock = World_GetBlock(ln.coords.x, ln.coords.y, ln.coords.z); + ln.brightness--; + if (ln.brightness == 0) continue; + + ln.coords.x--; + Light_TrySpreadInto(x, X, > , 0, isLamp, MAX, MIN) + ln.coords.x += 2; + Light_TrySpreadInto(x, X, < , World.MaxX, isLamp, MIN, MAX) + ln.coords.x--; + + ln.coords.y--; + Light_TrySpreadInto(y, Y, >, 0, isLamp, MAX, MIN) + ln.coords.y += 2; + Light_TrySpreadInto(y, Y, <, World.MaxY, isLamp, MIN, MAX) + ln.coords.y--; + + ln.coords.z--; + Light_TrySpreadInto(z, Z, > , 0, isLamp, MAX, MIN) + ln.coords.z += 2; + Light_TrySpreadInto(z, Z, < , World.MaxZ, isLamp, MIN, MAX) + } +} + +cc_uint8 GetBlockBrightness(BlockID curBlock, cc_bool isLamp) { + if (isLamp) return Blocks.Brightness[curBlock] >> FANCY_LIGHTING_LAMP_SHIFT; + return Blocks.Brightness[curBlock] & FANCY_LIGHTING_MAX_LEVEL; +} + +static void CalculateChunkLightingSelf(int chunkIndex, int cx, int cy, int cz) { + int x, y, z; + /* Block coordinates */ + int chunkStartX, chunkStartY, chunkStartZ, chunkEndX, chunkEndY, chunkEndZ; + cc_uint8 brightness; + BlockID curBlock; + chunkStartX = cx * CHUNK_SIZE; + chunkStartY = cy * CHUNK_SIZE; + chunkStartZ = cz * CHUNK_SIZE; + chunkEndX = chunkStartX + CHUNK_SIZE; + chunkEndY = chunkStartY + CHUNK_SIZE; + chunkEndZ = chunkStartZ + CHUNK_SIZE; + struct LightNode entry; + + if (chunkEndX > World.Width ) { chunkEndX = World.Width; } + if (chunkEndY > World.Height) { chunkEndY = World.Height; } + if (chunkEndZ > World.Length) { chunkEndZ = World.Length; } + + for (y = chunkStartY; y < chunkEndY; y++) { + for (z = chunkStartZ; z < chunkEndZ; z++) { + for (x = chunkStartX; x < chunkEndX; x++) { + + curBlock = World_GetBlock(x, y, z); + + if (Blocks.Brightness[curBlock] > 0) { + + brightness = GetBlockBrightness(curBlock, false); + + if (brightness > 0) { + entry = (struct LightNode){ { x, y, z }, brightness }; + Queue_Enqueue(&lightQueue, &entry); + FlushLightQueue(false, false); + } + else { + /* If no lava brightness, it must use lamp brightness */ + brightness = Blocks.Brightness[curBlock] >> FANCY_LIGHTING_LAMP_SHIFT; + entry = (struct LightNode){ { x, y, z }, brightness }; + Queue_Enqueue(&lightQueue, &entry); + FlushLightQueue(true, false); + } + } + + /* Note: This code only deals with generating light from block sources. + Regular sun light is added on as a "post process" step when returning light color in the exposed API. + This has the added benefit of being able to skip allocating chunk lighting data in regions that have no light-casting blocks*/ + } + } + } + + chunkLightingDataFlags[chunkIndex] = CHUNK_SELF_CALCULATED; +} + +static void CalculateChunkLightingAll(int chunkIndex, int cx, int cy, int cz) { + int x, y, z; + /* Chunk coordinates */ + int chunkStartX, chunkStartY, chunkStartZ; + int chunkEndX, chunkEndY, chunkEndZ; + int curChunkIndex; + + chunkStartX = cx - 1; + chunkStartY = cy - 1; + chunkStartZ = cz - 1; + chunkEndX = cx + 1; + chunkEndY = cy + 1; + chunkEndZ = cz + 1; + + if (chunkStartX == -1) { chunkStartX++; } + if (chunkStartY == -1) { chunkStartY++; } + if (chunkStartZ == -1) { chunkStartZ++; } + if (chunkEndX == World.ChunksX) { chunkEndX--; } + if (chunkEndY == World.ChunksY) { chunkEndY--; } + if (chunkEndZ == World.ChunksZ) { chunkEndZ--; } + + for (y = chunkStartY; y <= chunkEndY; y++) { + for (z = chunkStartZ; z <= chunkEndZ; z++) { + for (x = chunkStartX; x <= chunkEndX; x++) { + curChunkIndex = ChunkCoordsToIndex(x, y, z); + + if (chunkLightingDataFlags[curChunkIndex] == CHUNK_UNCALCULATED) { + CalculateChunkLightingSelf(curChunkIndex, x, y, z); + } + } + } + } + chunkLightingDataFlags[chunkIndex] = CHUNK_ALL_CALCULATED; +} + + +#define Light_TryUnSpreadInto(axis, dir, limit, AXIS, thisFace, thatFace) \ + if (neighborCoords.axis dir ## = limit && \ + CanLightPass(thisBlock, FACE_ ## AXIS ## thisFace) && \ + CanLightPass(World_GetBlock(neighborCoords.x, neighborCoords.y, neighborCoords.z), FACE_ ## AXIS ## thatFace) \ + ) \ + { \ + neighborBrightness = GetBrightness(neighborCoords.x, neighborCoords.y, neighborCoords.z, isLamp); \ + neighborBlockBrightness = GetBlockBrightness(World_GetBlock(neighborCoords.x, neighborCoords.y, neighborCoords.z), isLamp); \ + /* This spot is a light caster, mark this spot as needing to be re-spread */ \ + if (neighborBlockBrightness > 0) { \ + otherNode = (struct LightNode){ { neighborCoords.x, neighborCoords.y, neighborCoords.z }, neighborBlockBrightness }; \ + Queue_Enqueue(&lightQueue, &otherNode); \ + } \ + if (neighborBrightness > 0) { \ + /* This neighbor is darker than cur spot, darken it*/ \ + if (neighborBrightness < curNode.brightness) { \ + SetBrightness(0, neighborCoords.x, neighborCoords.y, neighborCoords.z, isLamp, true); \ + otherNode = (struct LightNode){ { neighborCoords.x, neighborCoords.y, neighborCoords.z }, neighborBrightness }; \ + Queue_Enqueue(&unlightQueue, &otherNode); \ + } \ + /* This neighbor is brighter or same, mark this spot as needing to be re-spread */ \ + else { \ + /* But only if the neighbor actually *can* spread to this block */ \ + if ( \ + CanLightPass(thisBlockTrue, FACE_ ## AXIS ## thisFace) && \ + CanLightPass(World_GetBlock(neighborCoords.x, neighborCoords.y, neighborCoords.z), FACE_ ## AXIS ## thatFace) \ + ) \ + { \ + otherNode = curNode; \ + otherNode.brightness = neighborBrightness-1; \ + Queue_Enqueue(&lightQueue, &otherNode); \ + } \ + } \ + } \ + } \ + +/* Spreads darkness out from this point and relights any necessary areas afterward */ +static void CalcUnlight(int x, int y, int z, cc_uint8 brightness, cc_bool isLamp) { + int count = 0; + struct LightNode curNode, otherNode; + cc_uint8 neighborBrightness, neighborBlockBrightness; + IVec3 neighborCoords; + BlockID thisBlockTrue, thisBlock; + + SetBrightness(0, x, y, z, isLamp, true); + curNode = (struct LightNode){ { x, y, z }, brightness }; + Queue_Enqueue(&unlightQueue, &curNode); + + while (unlightQueue.count > 0) { + curNode = *(struct LightNode*)(Queue_Dequeue(&unlightQueue)); + neighborCoords = curNode.coords; + + thisBlockTrue = World_GetBlock(neighborCoords.x, neighborCoords.y, neighborCoords.z); + /* For the original cell in the queue, assume this block is air + so that light can unspread "out" of it in the case of a solid blocks. */ + thisBlock = count == 0 ? BLOCK_AIR : thisBlockTrue; + + count++; + + neighborCoords.x--; + Light_TryUnSpreadInto(x, >, 0, X, MAX, MIN) + neighborCoords.x += 2; + Light_TryUnSpreadInto(x, <, World.MaxX, X, MIN, MAX) + neighborCoords.x--; + + neighborCoords.y--; + Light_TryUnSpreadInto(y, >, 0, Y, MAX, MIN) + neighborCoords.y += 2; + Light_TryUnSpreadInto(y, <, World.MaxY, Y, MIN, MAX) + neighborCoords.y--; + + neighborCoords.z--; + Light_TryUnSpreadInto(z, >, 0, Z, MAX, MIN) + neighborCoords.z += 2; + Light_TryUnSpreadInto(z, <, World.MaxZ, Z, MIN, MAX) + } + + FlushLightQueue(isLamp, true); +} +static void CalcBlockChange(int x, int y, int z, BlockID oldBlock, BlockID newBlock, cc_bool isLamp) { + cc_uint8 oldBlockLightLevel = GetBlockBrightness(oldBlock, isLamp); + cc_uint8 newBlockLightLevel = GetBlockBrightness(newBlock, isLamp); + cc_uint8 oldLightLevelHere = GetBrightness(x, y, z, isLamp); + struct LightNode entry; + + /* Cell has no lighting and new block doesn't cast light and blocks all light, no change */ + if (!oldLightLevelHere && !newBlockLightLevel && IsFullOpaque(newBlock)) return; + + /* Cell is darker than the new block, only brighter case */ + if (oldLightLevelHere < newBlockLightLevel) { + /* brighten this spot, recalculate lighting */ + entry = (struct LightNode){ { x, y, z }, newBlockLightLevel }; + Queue_Enqueue(&lightQueue, &entry); + FlushLightQueue(isLamp, true); + return; + } + + /* Light passes through old and new, old block does not cast light, new block does not cast light; no change */ + if (IsFullTransparent(oldBlock) && IsFullTransparent(newBlock) && !oldBlockLightLevel && !newBlockLightLevel) return; + + CalcUnlight(x, y, z, oldLightLevelHere, isLamp); +} +static void OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock) { + /* For some reason this is a possible case */ + if (oldBlock == newBlock) { return; } + + ClassicLighting_OnBlockChanged(x, y, z, oldBlock, newBlock); + + CalcBlockChange(x, y, z, oldBlock, newBlock, false); + CalcBlockChange(x, y, z, oldBlock, newBlock, true); +} +/* Invalidates/Resets lighting state for all of the blocks in the world */ +/* (e.g. because a block changed whether it is full bright or not) */ +static void Refresh(void) { + ClassicLighting_Refresh(); + FreeState(); + AllocState(); +} +static cc_bool IsLit(int x, int y, int z) { return ClassicLighting_IsLit(x, y, z); } +static cc_bool IsLit_Fast(int x, int y, int z) { return ClassicLighting_IsLit_Fast(x, y, z); } + +#define CalcForChunkIfNeeded(cx, cy, cz, chunkIndex) \ + if (chunkLightingDataFlags[chunkIndex] < CHUNK_ALL_CALCULATED) { \ + CalculateChunkLightingAll(chunkIndex, cx, cy, cz); \ + } + +static PackedCol Color_Core(int x, int y, int z, int paletteFace) { + cc_uint8 lightData; + int cx, cy, cz, chunkIndex; + int chunkCoordsIndex; + + cx = x >> CHUNK_SHIFT; + cy = y >> CHUNK_SHIFT; + cz = z >> CHUNK_SHIFT; + + chunkIndex = ChunkCoordsToIndex(cx, cy, cz); + CalcForChunkIfNeeded(cx, cy, cz, chunkIndex); + + /* There might be no light data in this chunk even after it was calculated */ + if (chunkLightingData[chunkIndex] == NULL) { + lightData = 0; + } else { + chunkCoordsIndex = GlobalCoordsToChunkCoordsIndex(x, y, z); + lightData = chunkLightingData[chunkIndex][chunkCoordsIndex]; + } + + /* This cell is exposed to sunlight */ + if (y > ClassicLighting_GetLightHeight(x, z)) { + /* Push the pointer forward into the sun lit palette section */ + paletteFace += PALETTE_SHADES; + } + + return palettes[paletteFace][lightData]; +} + +#define TRY_OOB_CASE(sun, shadow) if (!World_Contains(x, y, z)) return y >= Env.EdgeHeight ? sun : shadow +static PackedCol Color(int x, int y, int z) { + TRY_OOB_CASE(Env.SunCol, Env.ShadowCol); + return Color_Core(x, y, z, PALETTE_YMAX_INDEX); +} +static PackedCol Color_YMaxSide(int x, int y, int z) { + TRY_OOB_CASE(Env.SunCol, Env.ShadowCol); + return Color_Core(x, y, z, PALETTE_YMAX_INDEX); +} +static PackedCol Color_YMinSide(int x, int y, int z) { + TRY_OOB_CASE(Env.SunYMin, Env.ShadowYMin); + return Color_Core(x, y, z, PALETTE_YMIN_INDEX); +} +static PackedCol Color_XSide(int x, int y, int z) { + TRY_OOB_CASE(Env.SunXSide, Env.ShadowXSide); + return Color_Core(x, y, z, PALETTE_XSIDE_INDEX); +} +static PackedCol Color_ZSide(int x, int y, int z) { + TRY_OOB_CASE(Env.SunZSide, Env.ShadowZSide); + return Color_Core(x, y, z, PALETTE_ZSIDE_INDEX); +} + +static void LightHint(int startX, int startY, int startZ) { + int cx, cy, cz, chunkIndex; + ClassicLighting_LightHint(startX, startY, startZ); + /* Add 1 to startX/Z, as coordinates are for the extended chunk (18x18x18) */ + startX++; startY++; startZ++; + + cx = (startX + HALF_CHUNK_SIZE) >> CHUNK_SHIFT; + cy = (startY + HALF_CHUNK_SIZE) >> CHUNK_SHIFT; + cz = (startZ + HALF_CHUNK_SIZE) >> CHUNK_SHIFT; + + chunkIndex = ChunkCoordsToIndex(cx, cy, cz); + CalcForChunkIfNeeded(cx, cy, cz, chunkIndex); +} + +void FancyLighting_SetActive(void) { + Lighting.OnBlockChanged = OnBlockChanged; + Lighting.Refresh = Refresh; + Lighting.IsLit = IsLit; + Lighting.Color = Color; + Lighting.Color_XSide = Color_XSide; + + Lighting.IsLit_Fast = IsLit_Fast; + Lighting.Color_Sprite_Fast = Color; + Lighting.Color_YMax_Fast = Color; + Lighting.Color_YMin_Fast = Color_YMinSide; + Lighting.Color_XSide_Fast = Color_XSide; + Lighting.Color_ZSide_Fast = Color_ZSide; + + Lighting.FreeState = FreeState; + Lighting.AllocState = AllocState; + Lighting.LightHint = LightHint; +} + +static void OnEnvVariableChanged(void* obj, int envVar) { + /* This is always called, but should only do anything if fancy lighting is on */ + if (Lighting_Mode == LIGHTING_MODE_CLASSIC) { return; } + + if (envVar == ENV_VAR_SUN_COLOR || envVar == ENV_VAR_SHADOW_COLOR || envVar == ENV_VAR_LAVALIGHT_COLOR || envVar == ENV_VAR_LAMPLIGHT_COLOR) { + InitPalettes(); + } + if (envVar == ENV_VAR_LAVALIGHT_COLOR || envVar == ENV_VAR_LAMPLIGHT_COLOR) MapRenderer_Refresh(); +} + +void FancyLighting_OnInit(void) { + Event_Register_(&WorldEvents.EnvVarChanged, NULL, OnEnvVariableChanged); +} \ No newline at end of file diff --git a/src/Formats.c b/src/Formats.c new file mode 100644 index 0000000..2a86fd4 --- /dev/null +++ b/src/Formats.c @@ -0,0 +1,1897 @@ +#include "Formats.h" +#include "String.h" +#include "World.h" +#include "Deflate.h" +#include "Block.h" +#include "Entity.h" +#include "Platform.h" +#include "ExtMath.h" +#include "Logger.h" +#include "Game.h" +#include "Server.h" +#include "Event.h" +#include "Funcs.h" +#include "Errors.h" +#include "Stream.h" +#include "Chat.h" +#include "TexturePack.h" +#include "Utils.h" + +#ifdef CC_BUILD_FILESYSTEM +static struct LocationUpdate* spawn_point; +static struct MapImporter* imp_head; +static struct MapImporter* imp_tail; + + +/*########################################################################################################################* +*--------------------------------------------------------General----------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result Map_ReadBlocks(struct Stream* stream) { + World.Volume = World.Width * World.Length * World.Height; + World.Blocks = (BlockRaw*)Mem_TryAlloc(World.Volume, 1); + + if (!World.Blocks) return ERR_OUT_OF_MEMORY; + return Stream_Read(stream, World.Blocks, World.Volume); +} + +static cc_result Map_SkipGZipHeader(struct Stream* stream) { + struct GZipHeader gzHeader; + cc_result res; + GZipHeader_Init(&gzHeader); + + while (!gzHeader.done) { + if ((res = GZipHeader_Read(stream, &gzHeader))) return res; + } + return 0; +} + +void MapImporter_Register(struct MapImporter* imp) { + LinkedList_Append(imp, imp_head, imp_tail); +} + +struct MapImporter* MapImporter_Find(const cc_string* path) { + struct MapImporter* imp; + cc_string ext; + + for (imp = imp_head; imp; imp = imp->next) + { + ext = String_FromReadonly(imp->fileExt); + if (String_CaselessEnds(path, &ext)) return imp; + } + return NULL; +} + +cc_result Map_LoadFrom(const cc_string* path) { + cc_string relPath, fileName, fileExt; + struct LocationUpdate update = { 0 }; + struct MapImporter* imp; + struct Stream stream; + cc_result res; + Game_Reset(); + + spawn_point = &update; + res = Stream_OpenFile(&stream, path); + if (res) { Logger_SysWarn2(res, "opening", path); return res; } + + imp = MapImporter_Find(path); + if (!imp) { + res = ERR_NOT_SUPPORTED; + } else if ((res = imp->import(&stream))) { + World_Reset(); + } + + /* No point logging error for closing readonly file */ + (void)stream.Close(&stream); + if (res) Logger_SysWarn2(res, "decoding", path); + + World_SetNewMap(World.Blocks, World.Width, World.Height, World.Length); + if (!spawn_point) LocalPlayer_CalcDefaultSpawn(Entities.CurPlayer, &update); + LocalPlayers_MoveToSpawn(&update); + + relPath = *path; + Utils_UNSAFE_GetFilename(&relPath); + String_UNSAFE_Separate(&relPath, '.', &fileName, &fileExt); + String_Copy(&World.Name, &fileName); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------MCSharp level Format---------------------------------------------------* +*#########################################################################################################################*/ +#define LVL_CUSTOMTILE 163 +#define LVL_CHUNKSIZE 16 +/* MCSharp* format is a GZIP compressed binary map format. All metadata is discarded. + U16 "Identifier" (must be 1874) + U16 "Width", "Length", "Height" + U16 "SpawnX", "SpawnZ", "SpawnY" + U8 "Yaw", "Pitch" + U16 "Build permissions" (ignored) + U8* "Blocks" + + -- this data is only in MCGalaxy maps + U8 "Identifier" (0xBD for 'block definitions', i.e. custom blocks) + U8* "Data" (16x16x16 sparsely allocated chunks) +}*/ + +static const cc_uint8 Lvl_table[256] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 0, 0, 0, 0, 39, 36, 36, 10, 46, 21, 22, 22, 22, 22, + 4, 0, 22, 21, 0, 22, 23, 24, 22, 26, 27, 28, 30, 31, 32, 33, + 34, 35, 36, 22, 20, 49, 45, 1, 4, 0, 9, 11, 4, 19, 5, 17, + 10, 49, 20, 1, 18, 12, 5, 25, 46, 44, 17, 49, 20, 1, 18, 12, + 5, 25, 36, 34, 0, 9, 11, 46, 44, 0, 9, 11, 8, 10, 22, 27, + 22, 8, 10, 28, 17, 49, 20, 1, 18, 12, 5, 25, 46, 44, 11, 9, + 0, 9, 11, 163, 0, 0, 9, 11, 0, 0, 0, 0, 0, 0, 0, 28, + 22, 21, 11, 0, 0, 0, 46, 46, 10, 10, 46, 20, 41, 42, 11, 9, + 0, 8, 10, 10, 8, 0, 22, 22, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 21, 10, 0, 0, 0, 0, 0, 22, 22, 42, 3, 2, 29, + 47, 0, 0, 0, 0, 0, 27, 46, 48, 24, 22, 36, 34, 8, 10, 21, + 29, 22, 10, 22, 22, 41, 19, 35, 21, 29, 49, 34, 16, 41, 0, 22 +}; + +static cc_result Lvl_ReadCustomBlocks(struct Stream* stream) { + cc_uint8 chunk[LVL_CHUNKSIZE * LVL_CHUNKSIZE * LVL_CHUNKSIZE]; + cc_uint8 hasCustom; + int baseIndex, index, xx, yy, zz; + cc_result res; + int x, y, z, i; + + /* skip bounds checks when we know chunk is entirely inside map */ + int adjWidth = World.Width & ~0x0F; + int adjHeight = World.Height & ~0x0F; + int adjLength = World.Length & ~0x0F; + + for (y = 0; y < World.Height; y += LVL_CHUNKSIZE) { + for (z = 0; z < World.Length; z += LVL_CHUNKSIZE) { + for (x = 0; x < World.Width; x += LVL_CHUNKSIZE) { + + if ((res = stream->ReadU8(stream, &hasCustom))) return res; + if (hasCustom != 1) continue; + if ((res = Stream_Read(stream, chunk, sizeof(chunk)))) return res; + baseIndex = World_Pack(x, y, z); + + if ((x + LVL_CHUNKSIZE) <= adjWidth && (y + LVL_CHUNKSIZE) <= adjHeight && (z + LVL_CHUNKSIZE) <= adjLength) { + for (i = 0; i < sizeof(chunk); i++) { + xx = i & 0xF; yy = (i >> 8) & 0xF; zz = (i >> 4) & 0xF; + + index = baseIndex + World_Pack(xx, yy, zz); + World.Blocks[index] = World.Blocks[index] == LVL_CUSTOMTILE ? chunk[i] : World.Blocks[index]; + } + } else { + for (i = 0; i < sizeof(chunk); i++) { + xx = i & 0xF; yy = (i >> 8) & 0xF; zz = (i >> 4) & 0xF; + if ((x + xx) >= World.Width || (y + yy) >= World.Height || (z + zz) >= World.Length) continue; + + index = baseIndex + World_Pack(xx, yy, zz); + World.Blocks[index] = World.Blocks[index] == LVL_CUSTOMTILE ? chunk[i] : World.Blocks[index]; + } + } + } + } + } + return 0; +} + +/* Imports a world from a .lvl MCSharp server map file */ +/* Used by MCSharp/MCLawl/MCForge/MCDzienny/MCGalaxy */ +static cc_result Lvl_Load(struct Stream* stream) { + cc_uint8 header[18]; + cc_uint8* blocks; + cc_uint8 section; + cc_result res; + int i; + + struct Stream compStream; + struct InflateState state; + Inflate_MakeStream2(&compStream, &state, stream); + + if ((res = Map_SkipGZipHeader(stream))) return res; + if ((res = Stream_Read(&compStream, header, sizeof(header)))) return res; + if (Stream_GetU16_LE(&header[0]) != 1874) return LVL_ERR_VERSION; + + World.Width = Stream_GetU16_LE(&header[2]); + World.Length = Stream_GetU16_LE(&header[4]); + World.Height = Stream_GetU16_LE(&header[6]); + + spawn_point->flags = LU_HAS_POS | LU_HAS_YAW | LU_HAS_PITCH; + spawn_point->pos.x = Stream_GetU16_LE(&header[8]); + spawn_point->pos.z = Stream_GetU16_LE(&header[10]); + spawn_point->pos.y = Stream_GetU16_LE(&header[12]); + spawn_point->yaw = Math_Packed2Deg(header[14]); + spawn_point->pitch = Math_Packed2Deg(header[15]); + /* (2) pervisit, perbuild permissions */ + + if ((res = Map_ReadBlocks(&compStream))) return res; + blocks = World.Blocks; + /* Bulk convert 4 blocks at once */ + for (i = 0; i < (World.Volume & ~3); i += 4) { + *blocks = Lvl_table[*blocks]; blocks++; + *blocks = Lvl_table[*blocks]; blocks++; + *blocks = Lvl_table[*blocks]; blocks++; + *blocks = Lvl_table[*blocks]; blocks++; + } + for (; i < World.Volume; i++) { + *blocks = Lvl_table[*blocks]; blocks++; + } + + /* 0xBD section type is not present in older .lvl files */ + res = compStream.ReadU8(&compStream, §ion); + if (res == ERR_END_OF_STREAM) return 0; + + if (res) return res; + /* Unrecognised section type, stop reading */ + if (section != 0xBD) return 0; + + res = Lvl_ReadCustomBlocks(&compStream); + /* At least one map out there has a corrupted 0xBD section */ + if (res == ERR_END_OF_STREAM) { + Chat_AddRaw("&cEnd of stream reading .lvl custom blocks section"); + Chat_AddRaw("&c Some blocks may therefore appear incorrectly"); + res = 0; + } + return res; +} + + +/*########################################################################################################################* +*----------------------------------------------------fCraft map format----------------------------------------------------* +*#########################################################################################################################*/ +/* fCraft* format is a binary map format. All metadata is discarded. + U32 "Identifier" (must be FC2AF40) + U8 "Revision" (only '13' supported) + U16 "Width", "Height", "Length" + U32 "SpawnX", "SpawnY", "SpawnZ" + U8 "Yaw", "Pitch" + U32 "DateModified", "DateCreated" (ignored) + U8* "UUID" + U8 "Layers" (only maps with 1 layer supported) + U8* "LayersInfo" (ignored, assumes only layer is map blocks) + U32 "MetaCount" + METADATA { STR "Group", "Key", "Value" } + U8* "Blocks" +}*/ +static cc_result Fcm_ReadString(struct Stream* stream) { + cc_uint8 data[2]; + int len; + cc_result res; + + if ((res = Stream_Read(stream, data, sizeof(data)))) return res; + len = Stream_GetU16_LE(data); + + return stream->Skip(stream, len); +} + +/* Imports a world from a .fcm fCraft server map file (v3 only) */ +/* Used by fCraft/800Craft/LegendCraft/ProCraft */ +static cc_result Fcm_Load(struct Stream* stream) { + cc_uint8 header[79]; + cc_result res; + int i, count; + + struct Stream compStream; + struct InflateState state; + Inflate_MakeStream2(&compStream, &state, stream); + + if ((res = Stream_Read(stream, header, sizeof(header)))) return res; + if (Stream_GetU32_LE(&header[0]) != 0x0FC2AF40UL) return FCM_ERR_IDENTIFIER; + if (header[4] != 13) return FCM_ERR_REVISION; + + World.Width = Stream_GetU16_LE(&header[5]); + World.Height = Stream_GetU16_LE(&header[7]); + World.Length = Stream_GetU16_LE(&header[9]); + + spawn_point->flags = LU_HAS_POS | LU_HAS_YAW | LU_HAS_PITCH; + spawn_point->pos.x = ((int)Stream_GetU32_LE(&header[11])) / 32.0f; + spawn_point->pos.y = ((int)Stream_GetU32_LE(&header[15])) / 32.0f; + spawn_point->pos.z = ((int)Stream_GetU32_LE(&header[19])) / 32.0f; + spawn_point->yaw = Math_Packed2Deg(header[23]); + spawn_point->pitch = Math_Packed2Deg(header[24]); + + /* header[25] (4) date modified */ + /* header[29] (4) date created */ + Mem_Copy(&World.Uuid, &header[33], WORLD_UUID_LEN); + /* header[49] (26) layer index */ + count = (int)Stream_GetU32_LE(&header[75]); + + /* header isn't compressed, rest of data is though */ + for (i = 0; i < count; i++) { + if ((res = Fcm_ReadString(&compStream))) return res; /* Group */ + if ((res = Fcm_ReadString(&compStream))) return res; /* Key */ + if ((res = Fcm_ReadString(&compStream))) return res; /* Value */ + } + + return Map_ReadBlocks(&compStream); +} + + +/*########################################################################################################################* +*---------------------------------------------------------NBTFile---------------------------------------------------------* +*#########################################################################################################################*/ +enum NbtTagType { + NBT_END, NBT_I8, NBT_I16, NBT_I32, NBT_I64, NBT_F32, + NBT_F64, NBT_I8S, NBT_STR, NBT_LIST, NBT_DICT +}; + +#define NBT_SMALL_SIZE STRING_SIZE +#define NBT_STRING_SIZE STRING_SIZE + +#define NbtTag_IsSmall(tag) ((tag)->dataSize <= NBT_SMALL_SIZE) +#define IsTag(tag, tagName) (String_CaselessEqualsConst(&tag->name, tagName)) +struct NbtTag; + +struct NbtTag { + struct NbtTag* parent; + cc_uint8 type; + cc_string name; + cc_uint32 dataSize; /* size of data for arrays */ + + union { + cc_uint8 u8; + cc_int16 i16; + cc_uint16 u16; + cc_int32 i32; + cc_uint32 u32; + float f32; + cc_uint8 small[NBT_SMALL_SIZE]; + cc_uint8* big; /* malloc for big byte arrays */ + struct { cc_string text; char buffer[STRING_SIZE * 2]; } str; + } value; + char _nameBuffer[NBT_STRING_SIZE]; + cc_result result; + int listIndex; +}; + +static cc_uint8 NbtTag_U8(struct NbtTag* tag) { + if (tag->type == NBT_I8) return tag->value.u8; + + tag->result = NBT_ERR_EXPECTED_I8; + return 0; +} + +static cc_int16 NbtTag_I16(struct NbtTag* tag) { + if (tag->type == NBT_I16) return tag->value.i16; + if (tag->type == NBT_I8) return tag->value.u8; + + tag->result = NBT_ERR_EXPECTED_I16; + return 0; +} + +static cc_uint16 NbtTag_U16(struct NbtTag* tag) { + if (tag->type == NBT_I16) return tag->value.u16; + if (tag->type == NBT_I8) return tag->value.u8; + + tag->result = NBT_ERR_EXPECTED_I16; + return 0; +} + +static int NbtTag_I32(struct NbtTag* tag) { + if (tag->type == NBT_I32) return tag->value.i32; + if (tag->type == NBT_I16) return tag->value.i16; + if (tag->type == NBT_I8) return tag->value.u8; + + tag->result = NBT_ERR_EXPECTED_I32; + return 0; +} + +static float NbtTag_F32(struct NbtTag* tag) { + if (tag->type == NBT_F32) return tag->value.f32; + + tag->result = NBT_ERR_EXPECTED_F32; + return 0; +} + +static cc_uint8* NbtTag_U8_Array(struct NbtTag* tag, int minSize) { + if (tag->type != NBT_I8S) { tag->result = NBT_ERR_EXPECTED_ARR; return NULL; } + if (tag->dataSize < minSize) { tag->result = NBT_ERR_ARR_TOO_SMALL; return NULL; } + + return NbtTag_IsSmall(tag) ? tag->value.small : tag->value.big; +} + +static cc_string NbtTag_String(struct NbtTag* tag) { + if (tag->type == NBT_STR) return tag->value.str.text; + + tag->result = NBT_ERR_EXPECTED_STR; + return String_Empty; +} + +static cc_result Nbt_ReadString(struct Stream* stream, cc_string* str) { + cc_uint8 buffer[NBT_STRING_SIZE * 4]; + int len; + cc_result res; + + if ((res = Stream_Read(stream, buffer, 2))) return res; + len = Stream_GetU16_BE(buffer); + + if (len > Array_Elems(buffer)) return CW_ERR_STRING_LEN; + if ((res = Stream_Read(stream, buffer, len))) return res; + + String_AppendUtf8(str, buffer, len); + return 0; +} + +typedef void (*Nbt_Callback)(struct NbtTag* tag); +static cc_result Nbt_ReadTag(cc_uint8 typeId, cc_bool readTagName, struct Stream* stream, + struct NbtTag* parent, Nbt_Callback callback, int listIndex) { + struct NbtTag tag; + cc_uint8 childType; + cc_uint8 tmp[5]; + cc_result res; + cc_uint32 i, count; + + if (typeId == NBT_END) return 0; + tag.type = typeId; + tag.parent = parent; + tag.dataSize = 0; + tag.listIndex = listIndex; + String_InitArray(tag.name, tag._nameBuffer); + + if (readTagName) { + res = Nbt_ReadString(stream, &tag.name); + if (res) return res; + } + + switch (typeId) { + case NBT_I8: + res = stream->ReadU8(stream, &tag.value.u8); + break; + case NBT_I16: + res = Stream_Read(stream, tmp, 2); + tag.value.u16 = Stream_GetU16_BE(tmp); + break; + case NBT_I32: + case NBT_F32: + res = Stream_ReadU32_BE(stream, &tag.value.u32); + break; + case NBT_I64: + case NBT_F64: + res = stream->Skip(stream, 8); + break; /* (8) data */ + + case NBT_I8S: + if ((res = Stream_ReadU32_BE(stream, &tag.dataSize))) break; + + if (NbtTag_IsSmall(&tag)) { + res = Stream_Read(stream, tag.value.small, tag.dataSize); + } else { + tag.value.big = (cc_uint8*)Mem_TryAlloc(tag.dataSize, 1); + if (!tag.value.big) return ERR_OUT_OF_MEMORY; + + res = Stream_Read(stream, tag.value.big, tag.dataSize); + if (res) Mem_Free(tag.value.big); + } + break; + case NBT_STR: + String_InitArray(tag.value.str.text, tag.value.str.buffer); + res = Nbt_ReadString(stream, &tag.value.str.text); + break; + + case NBT_LIST: + if ((res = Stream_Read(stream, tmp, 5))) break; + childType = tmp[0]; + count = Stream_GetU32_BE(&tmp[1]); + + for (i = 0; i < count; i++) { + res = Nbt_ReadTag(childType, false, stream, &tag, callback, i); + if (res) break; + } + break; + + case NBT_DICT: + for (;;) { + if ((res = stream->ReadU8(stream, &childType))) break; + if (childType == NBT_END) break; + + res = Nbt_ReadTag(childType, true, stream, &tag, callback, 0); + if (res) break; + } + break; + + default: return NBT_ERR_UNKNOWN; + } + + if (res) return res; + tag.result = 0; + callback(&tag); + /* NOTE: callback must set DataBig to NULL, if doesn't want it to be freed */ + if (!NbtTag_IsSmall(&tag)) Mem_Free(tag.value.big); + return tag.result; +} + + +static BlockRaw* Nbt_TakeArray(struct NbtTag* tag, const char* type) { + BlockRaw* ptr; + if (NbtTag_IsSmall(tag)) { + /* Small data is stored inline in tha tag, so need to copy it out */ + ptr = (BlockRaw*)Mem_Alloc(tag->dataSize, 1, type); + Mem_Copy(ptr, tag->value.small, tag->dataSize); + } else { + ptr = tag->value.big; + tag->value.big = NULL; /* So Nbt_ReadTag doesn't call Mem_Free on the array */ + } + return ptr; +} + +static cc_result Nbt_Read(struct Stream* stream, Nbt_Callback callback) { + struct Stream compStream; + struct InflateState state; + cc_result res; + cc_uint8 tag; + + Inflate_MakeStream2(&compStream, &state, stream); + if ((res = Map_SkipGZipHeader(stream))) return res; + if ((res = compStream.ReadU8(&compStream, &tag))) return res; + + if (tag != NBT_DICT) return CW_ERR_ROOT_TAG; + return Nbt_ReadTag(NBT_DICT, true, &compStream, NULL, callback, 0); +} + + +/*########################################################################################################################* +*--------------------------------------------------------NBTWriter--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint8* Nbt_WriteConst(cc_uint8* data, const char* text) { + int i, len = String_Length(text); + *data++ = 0; + *data++ = (cc_uint8)len; + + for (i = 0; i < len; i++) { *data++ = text[i]; } + return data; +} + +static cc_uint8* Nbt_WriteString(cc_uint8* data, const char* name, const cc_string* text) { + cc_uint8* start; int i; + + *data++ = NBT_STR; + data = Nbt_WriteConst(data, name); + start = data; + data += 2; /* length written later */ + + for (i = 0; i < text->length; i++) { + data = Convert_CP437ToUtf8(text->buffer[i], data) + data; + } + + Stream_SetU16_BE(start, (int)(data - start) - 2); + return data; +} + +static cc_uint8* Nbt_WriteDict(cc_uint8* data, const char* name) { + *data++ = NBT_DICT; + data = Nbt_WriteConst(data, name); + + return data; +} + +static cc_uint8* Nbt_WriteArray(cc_uint8* data, const char* name, int size) { + *data++ = NBT_I8S; + data = Nbt_WriteConst(data, name); + + Stream_SetU32_BE(data, size); + return data + 4; +} + +static cc_uint8* Nbt_WriteUInt8(cc_uint8* data, const char* name, cc_uint8 value) { + *data++ = NBT_I8; + data = Nbt_WriteConst(data, name); + + *data = value; + return data + 1; +} + +static cc_uint8* Nbt_WriteUInt16(cc_uint8* data, const char* name, cc_uint16 value) { + *data++ = NBT_I16; + data = Nbt_WriteConst(data, name); + + Stream_SetU16_BE(data, value); + return data + 2; +} + +static cc_uint8* Nbt_WriteInt32(cc_uint8* data, const char* name, int value) { + *data++ = NBT_I32; + data = Nbt_WriteConst(data, name); + + Stream_SetU32_BE(data, value); + return data + 4; +} + +static cc_uint8* Nbt_WriteFloat(cc_uint8* data, const char* name, float value) { + union IntAndFloat raw; + *data++ = NBT_F32; + data = Nbt_WriteConst(data, name); + + raw.f = value; + Stream_SetU32_BE(data, raw.u); + return data + 4; +} + + +/*########################################################################################################################* +*--------------------------------------------------ClassicWorld format----------------------------------------------------* +*#########################################################################################################################*/ +/* ClassicWorld is a NBT tag based map format. Tags not listed below are discarded. +COMPOUND "ClassicWorld" { + U8* "UUID" + U16 "X", "Y", "Z" + COMPOUND "Spawn" { + I16 "X", "Y", "Z" + U8 "H", "P" + } + U8* "BlockArray" (lower 8 bits, required) + U8* "BlockArray2" (upper 8 bits, optional) + COMPOUND "Metadata" { + COMPOUND "CPE" { + COMPOUND "ClickDistance" { U16 "Reach" } + COMPOUND "EnvWeatherType" { U8 "WeatherType" } + COMPOUND "EnvMapAppearance" { + U8 "SideBlock", "EdgeBlock" + I16 "SidesLevel" + STR "TextureURL" + } + COMPOUND "EnvColors" { + COMPOUND "Sky" { U16 "R", "G", "B" } + COMPOUND "Cloud" { U16 "R", "G", "B" } + COMPOUND "Fog" { U16 "R", "G", "B" } + COMPOUND "Sunlight" { U16 "R", "G", "B" } + COMPOUND "Ambient" { U16 "R", "G", "B" } + } + COMPOUND "BlockDefinitions" { + COMPOUND "Block_XYZ" { (name must start with 'Block') + U8 "ID", U16 "ID2" + STR "Name" + F32 "Speed" + U8 "CollideType", "BlockDraw" + U8 "TransmitsLight", "FullBright" + U8 "Shape" , "WalkSound" + U8* "Textures", "Fog", "Coords" + } + } + } + } +}*/ + +static void Cw_Callback_1(struct NbtTag* tag) { + if (IsTag(tag, "X")) { World.Width = NbtTag_U16(tag); return; } + if (IsTag(tag, "Y")) { World.Height = NbtTag_U16(tag); return; } + if (IsTag(tag, "Z")) { World.Length = NbtTag_U16(tag); return; } + + if (IsTag(tag, "UUID")) { + if (tag->dataSize != WORLD_UUID_LEN) { + tag->result = CW_ERR_UUID_LEN; + } else { + Mem_Copy(World.Uuid, tag->value.small, WORLD_UUID_LEN); + } + return; + } + + if (IsTag(tag, "BlockArray")) { + World.Volume = tag->dataSize; + World.Blocks = Nbt_TakeArray(tag, ".cw map blocks"); + } +#ifdef EXTENDED_BLOCKS + if (IsTag(tag, "BlockArray2")) { + World_SetMapUpper(Nbt_TakeArray(tag, ".cw map blocks2")); + } +#endif +} + +static void Cw_Callback_2(struct NbtTag* tag) { + if (IsTag(tag->parent, "MapGenerator")) { + if (IsTag(tag, "Seed")) { World.Seed = NbtTag_I32(tag); return; } + return; + } + if (!IsTag(tag->parent, "Spawn")) return; + spawn_point->flags = LU_HAS_POS | LU_HAS_YAW | LU_HAS_PITCH; + + if (IsTag(tag, "X")) { spawn_point->pos.x = NbtTag_I16(tag); return; } + if (IsTag(tag, "Y")) { spawn_point->pos.y = NbtTag_I16(tag); return; } + if (IsTag(tag, "Z")) { spawn_point->pos.z = NbtTag_I16(tag); return; } + if (IsTag(tag, "H")) { spawn_point->yaw = Math_Packed2Deg(NbtTag_U8(tag)); return; } + if (IsTag(tag, "P")) { spawn_point->pitch = Math_Packed2Deg(NbtTag_U8(tag)); return; } +} + +static BlockID cw_curID; +static int cw_colR, cw_colG, cw_colB; +static PackedCol Cw_ParseColor(PackedCol defValue) { + int r = cw_colR, g = cw_colG, b = cw_colB; + if (r > 255 || g > 255 || b > 255) return defValue; + return PackedCol_Make(r, g, b, 255); +} + +static void Cw_Callback_4(struct NbtTag* tag) { + BlockID id = cw_curID; + struct LocalPlayer* p = &LocalPlayer_Instances[0]; + + if (!IsTag(tag->parent->parent, "CPE")) return; + if (!IsTag(tag->parent->parent->parent, "Metadata")) return; + + if (IsTag(tag->parent, "ClickDistance")) { + if (IsTag(tag, "Distance")) { p->ReachDistance = NbtTag_U16(tag) / 32.0f; return; } + } + if (IsTag(tag->parent, "EnvWeatherType")) { + if (IsTag(tag, "WeatherType")) { Env.Weather = NbtTag_U8(tag); return; } + } + + if (IsTag(tag->parent, "EnvMapAppearance")) { + if (IsTag(tag, "SideBlock")) { Env.SidesBlock = NbtTag_U8(tag); return; } + if (IsTag(tag, "EdgeBlock")) { Env.EdgeBlock = NbtTag_U8(tag); return; } + if (IsTag(tag, "SideLevel")) { Env.EdgeHeight = NbtTag_I16(tag); return; } + + if (IsTag(tag, "TextureURL")) { + cc_string url = NbtTag_String(tag); + if (url.length) Server_RetrieveTexturePack(&url); + return; + } + } + + if (IsTag(tag->parent, "EnvMapAspect")) { + if (IsTag(tag, "EdgeBlock")) { Env.EdgeBlock = NbtTag_U16(tag); return; } + if (IsTag(tag, "SideBlock")) { Env.SidesBlock = NbtTag_U16(tag); return; } + if (IsTag(tag, "EdgeHeight")) { Env.EdgeHeight = NbtTag_I32(tag); return; } + if (IsTag(tag, "SidesOffset")) { Env.SidesOffset = NbtTag_I32(tag); return; } + if (IsTag(tag, "CloudsHeight")) { Env.CloudsHeight = NbtTag_I32(tag); return; } + if (IsTag(tag, "CloudsSpeed")) { Env.CloudsSpeed = NbtTag_F32(tag); return; } + if (IsTag(tag, "WeatherSpeed")) { Env.WeatherSpeed = NbtTag_F32(tag); return; } + if (IsTag(tag, "WeatherFade")) { Env.WeatherFade = NbtTag_F32(tag); return; } + if (IsTag(tag, "ExpFog")) { Env.ExpFog = NbtTag_U8(tag); return; } + if (IsTag(tag, "SkyboxHor")) { Env.SkyboxHorSpeed = NbtTag_F32(tag); return; } + if (IsTag(tag, "SkyboxVer")) { Env.SkyboxVerSpeed = NbtTag_F32(tag); return; } + } + + /* Callback for compound tag is called after all its children have been processed */ + if (IsTag(tag->parent, "EnvColors")) { + if (IsTag(tag, "Sky")) { + Env.SkyCol = Cw_ParseColor(ENV_DEFAULT_SKY_COLOR); return; + } else if (IsTag(tag, "Cloud")) { + Env.CloudsCol = Cw_ParseColor(ENV_DEFAULT_CLOUDS_COLOR); return; + } else if (IsTag(tag, "Fog")) { + Env.FogCol = Cw_ParseColor(ENV_DEFAULT_FOG_COLOR); return; + } else if (IsTag(tag, "Sunlight")) { + Env_SetSunCol(Cw_ParseColor(ENV_DEFAULT_SUN_COLOR)); return; + } else if (IsTag(tag, "Ambient")) { + Env_SetShadowCol(Cw_ParseColor(ENV_DEFAULT_SHADOW_COLOR)); return; + } else if (IsTag(tag, "Skybox")) { + Env.SkyboxCol = Cw_ParseColor(ENV_DEFAULT_SKYBOX_COLOR); return; + } + } + + if (IsTag(tag->parent, "BlockDefinitions") && Game_AllowCustomBlocks) { + static const cc_string blockStr = String_FromConst("Block"); + if (!String_CaselessStarts(&tag->name, &blockStr)) return; + + /* hack for sprite draw (can't rely on order of tags when reading) */ + if (Blocks.SpriteOffset[id] == 0) { + Blocks.SpriteOffset[id] = Blocks.Draw[id]; + Blocks.Draw[id] = DRAW_SPRITE; + } else { + Blocks.SpriteOffset[id] = 0; + } + + Block_DefineCustom(id, false); + Blocks.CanPlace[id] = true; + Blocks.CanDelete[id] = true; + Event_RaiseVoid(&BlockEvents.PermissionsChanged); + + cw_curID = 0; + } +} + +static void Cw_Callback_5(struct NbtTag* tag) { + BlockID id = cw_curID; + cc_uint8* arr; + cc_uint8 sound; + + if (!IsTag(tag->parent->parent->parent, "CPE")) return; + if (!IsTag(tag->parent->parent->parent->parent, "Metadata")) return; + + if (IsTag(tag->parent->parent, "EnvColors")) { + if (IsTag(tag, "R")) { cw_colR = NbtTag_U16(tag); return; } + if (IsTag(tag, "G")) { cw_colG = NbtTag_U16(tag); return; } + if (IsTag(tag, "B")) { cw_colB = NbtTag_U16(tag); return; } + } + + if (IsTag(tag->parent->parent, "BlockDefinitions") && Game_AllowCustomBlocks) { + if (IsTag(tag, "ID")) { cw_curID = NbtTag_U8(tag); return; } + if (IsTag(tag, "ID2")) { cw_curID = NbtTag_U16(tag); return; } + if (IsTag(tag, "CollideType")) { Blocks.Collide[id] = NbtTag_U8(tag); return; } + if (IsTag(tag, "Speed")) { Blocks.SpeedMultiplier[id] = NbtTag_F32(tag); return; } + if (IsTag(tag, "TransmitsLight")) { Blocks.BlocksLight[id] = NbtTag_U8(tag) == 0; return; } + if (IsTag(tag, "FullBright")) { Blocks.Brightness[id] = Block_ReadBrightness(NbtTag_U8(tag)); return; } + if (IsTag(tag, "BlockDraw")) { Blocks.Draw[id] = NbtTag_U8(tag); return; } + if (IsTag(tag, "Shape")) { Blocks.SpriteOffset[id] = NbtTag_U8(tag); return; } + + if (IsTag(tag, "Name")) { + cc_string name = NbtTag_String(tag); + Block_SetName(id, &name); + return; + } + + if (IsTag(tag, "Textures")) { + arr = NbtTag_U8_Array(tag, 6); + if (!arr) return; + + Block_Tex(id, FACE_YMAX) = arr[0]; Block_Tex(id, FACE_YMIN) = arr[1]; + Block_Tex(id, FACE_XMIN) = arr[2]; Block_Tex(id, FACE_XMAX) = arr[3]; + Block_Tex(id, FACE_ZMIN) = arr[4]; Block_Tex(id, FACE_ZMAX) = arr[5]; + + /* hacky way of storing upper 8 bits */ + if (tag->dataSize >= 12) { + Block_Tex(id, FACE_YMAX) |= arr[6] << 8; Block_Tex(id, FACE_YMIN) |= arr[7] << 8; + Block_Tex(id, FACE_XMIN) |= arr[8] << 8; Block_Tex(id, FACE_XMAX) |= arr[9] << 8; + Block_Tex(id, FACE_ZMIN) |= arr[10] << 8; Block_Tex(id, FACE_ZMAX) |= arr[11] << 8; + } + return; + } + + if (IsTag(tag, "WalkSound")) { + sound = NbtTag_U8(tag); + Blocks.DigSounds[id] = sound; + Blocks.StepSounds[id] = sound; + if (sound == SOUND_GLASS) Blocks.StepSounds[id] = SOUND_STONE; + return; + } + + if (IsTag(tag, "Fog")) { + arr = NbtTag_U8_Array(tag, 4); + if (!arr) return; + + Blocks.FogDensity[id] = (arr[0] + 1) / 128.0f; + /* Backwards compatibility with apps that use 0xFF to indicate no fog */ + if (arr[0] == 0 || arr[0] == 0xFF) Blocks.FogDensity[id] = 0.0f; + Blocks.FogCol[id] = PackedCol_Make(arr[1], arr[2], arr[3], 255); + return; + } + + if (IsTag(tag, "Coords")) { + arr = NbtTag_U8_Array(tag, 6); + if (!arr) return; + + Blocks.MinBB[id].x = (cc_int8)arr[0] / 16.0f; Blocks.MaxBB[id].x = (cc_int8)arr[3] / 16.0f; + Blocks.MinBB[id].y = (cc_int8)arr[1] / 16.0f; Blocks.MaxBB[id].y = (cc_int8)arr[4] / 16.0f; + Blocks.MinBB[id].z = (cc_int8)arr[2] / 16.0f; Blocks.MaxBB[id].z = (cc_int8)arr[5] / 16.0f; + return; + } + } +} + +static void Cw_Callback(struct NbtTag* tag) { + struct NbtTag* tmp = tag->parent; + int depth = 0; + while (tmp) { depth++; tmp = tmp->parent; } + + switch (depth) { + case 1: Cw_Callback_1(tag); return; + case 2: Cw_Callback_2(tag); return; + case 4: Cw_Callback_4(tag); return; + case 5: Cw_Callback_5(tag); return; + } + /* ClassicWorld -> Metadata -> CPE -> ExtName -> [values] + 0 1 2 3 4 */ +} + +/* Imports a world from a .cw ClassicWorld map file */ +/* Used by ClassiCube/ClassicalSharp */ +static cc_result Cw_Load(struct Stream* stream) { + return Nbt_Read(stream, Cw_Callback); +} + + +/*########################################################################################################################* +*-----------------------------------------------Java serialisation format-------------------------------------------------* +*#########################################################################################################################*/ +/* Rather than bothering following this, I skip a lot of the java serialisation format + Stream BlockData BlockDataTiny BlockDataLong +|--------------| |---------------| |---------------| |---------------| +| U16 Magic | |>BlockDataTiny | | TC_BLOCKDATA | | TC_BLOCKLONG | +| U16 Version | |>BlockDataLong | | U8 Size | | U32 Size | +| Content[var] | |_______________| | U8 Data[size] | | U8 Data[size] | +|______________| |_______________| |_______________| + + Content +|--------------| |--------------| +| >BlockData | | >NewString | +| >Object | | >TC_RESET | +|______________| | >TC_NULL | +| >PrevObject | +| >NewClass | +| >NewEnum | + +}*/ +enum JTypeCode { + TC_NULL = 0x70, TC_REFERENCE = 0x71, TC_CLASSDESC = 0x72, TC_OBJECT = 0x73, + TC_STRING = 0x74, TC_ARRAY = 0x75, TC_BLOCKDATA = 0x77, TC_ENDBLOCKDATA = 0x78 +}; +enum JFieldType { + JFIELD_I8 = 'B', JFIELD_F64 = 'D', JFIELD_F32 = 'F', JFIELD_I32 = 'I', JFIELD_I64 = 'J', + JFIELD_BOOL = 'Z', JFIELD_ARRAY = '[', JFIELD_OBJECT = 'L' +}; + +#define JNAME_SIZE 48 +#define SC_WRITE_METHOD 0x01 +#define SC_SERIALIZABLE 0x02 + +static cc_uint32 reference_id; +#define Java_AddReference() reference_id++; + +union JValue { + cc_uint8 U8; + cc_int32 I32; + cc_uint32 U32; + float F32; + struct { cc_uint8* Ptr; cc_uint32 Size; } Array; +}; + +struct JFieldDesc { + cc_uint8 Type; + cc_uint8 FieldName[JNAME_SIZE]; + union JValue Value; + /* "Value" field here is not accurate to how java deserialising actually works, */ + /* but easier to store here since only care about Level class values anyways */ +}; + +struct JClassDesc; +struct JClassDesc { + cc_uint8 ClassName[JNAME_SIZE]; + cc_uint8 Flags; + int FieldsCount; + struct JFieldDesc Fields[38]; + cc_uint32 Reference; + struct JClassDesc* SuperClass; + struct JClassDesc* tmp; +}; + +struct JArray { + struct JClassDesc* Desc; + cc_uint8* Data; /* for byte arrays */ + cc_uint32 Size; /* for byte arrays */ +}; + +struct JUnion { + cc_uint8 Type; + union { + cc_uint8 String[JNAME_SIZE]; /* TC_STRING */ + struct JClassDesc* Object; /* TC_OBJECT */ + struct JArray Array; /* TC_ARRAY */ + } Value; +}; + +static cc_result Java_ReadString(struct Stream* stream, cc_uint8* buffer) { + int len; + cc_result res; + + if ((res = Stream_Read(stream, buffer, 2))) return res; + len = Stream_GetU16_BE(buffer); + + Mem_Set(buffer, 0, JNAME_SIZE); + if (len > JNAME_SIZE) return JAVA_ERR_JSTRING_LEN; + return Stream_Read(stream, buffer, len); +} + + +static cc_result Java_ReadObject(struct Stream* stream, struct JUnion* object); +static cc_result Java_ReadObjectData(struct Stream* stream, struct JUnion* object); +static cc_result Java_SkipAnnotation(struct Stream* stream) { + cc_uint8 typeCode, count; + struct JUnion object; + cc_result res; + + for (;;) + { + if ((res = stream->ReadU8(stream, &typeCode))) return res; + + switch (typeCode) + { + case TC_BLOCKDATA: + if ((res = stream->ReadU8(stream, &count))) return res; + if ((res = stream->Skip(stream, count))) return res; + break; + case TC_ENDBLOCKDATA: + return 0; + default: + object.Type = typeCode; + if ((res = Java_ReadObjectData(stream, &object))) return res; + break; + } + } + return 0; +} + + +/* Most .dat maps only use at most 16 different class types */ +/* However some survival test maps can use up to 30 */ +#define CLASS_CAPACITY 30 +static struct JClassDesc* class_cache; +static int class_count; +static cc_result Java_ReadClassDesc(struct Stream* stream, struct JClassDesc** desc); + +static cc_result Java_ReadFieldDesc(struct Stream* stream, struct JFieldDesc* desc) { + struct JUnion className; + cc_result res; + + if ((res = stream->ReadU8(stream, &desc->Type))) return res; + if ((res = Java_ReadString(stream, desc->FieldName))) return res; + + if (desc->Type == JFIELD_ARRAY || desc->Type == JFIELD_OBJECT) { + return Java_ReadObject(stream, &className); + } + return 0; +} + +static cc_result Java_ReadNewClassDesc(struct Stream* stream, struct JClassDesc* desc) { + cc_uint8 count[2]; + cc_result res; + int i; + + if ((res = Java_ReadString(stream, desc->ClassName))) return res; + if ((res = stream->Skip(stream, 8))) return res; /* serial version UID */ + if ((res = stream->ReadU8(stream, &desc->Flags))) return res; + + desc->Reference = reference_id; + Java_AddReference(); + + if ((res = Stream_Read(stream, count, 2))) return res; + desc->FieldsCount = Stream_GetU16_BE(count); + if (desc->FieldsCount > Array_Elems(desc->Fields)) return JAVA_ERR_JCLASS_FIELDS; + + for (i = 0; i < desc->FieldsCount; i++) { + if ((res = Java_ReadFieldDesc(stream, &desc->Fields[i]))) return res; + } + + if ((res = Java_SkipAnnotation(stream))) return res; + return Java_ReadClassDesc(stream, &desc->SuperClass); +} + +static cc_result Java_ReadClassDesc(struct Stream* stream, struct JClassDesc** desc) { + cc_uint8 typeCode; + cc_uint32 reference; + cc_result res; + int i; + if ((res = stream->ReadU8(stream, &typeCode))) return res; + + switch (typeCode) + { + case TC_NULL: + *desc = NULL; + return 0; + + case TC_REFERENCE: + if ((res = Stream_ReadU32_BE(stream, &reference))) return res; + + /* Use a previously defined ClassDescriptor */ + for (i = 0; i < class_count; i++) + { + if (class_cache[i].Reference != reference) continue; + + *desc = &class_cache[i]; + return 0; + } + return JAVA_ERR_JCLASS_REFERENCE; + + case TC_CLASSDESC: + if (class_count >= CLASS_CAPACITY) return JAVA_ERR_JCLASSES_COUNT; + + *desc = &class_cache[class_count++]; + return Java_ReadNewClassDesc(stream, *desc); + } + return JAVA_ERR_JCLASS_TYPE; +} + + +static cc_result Java_ReadValue(struct Stream* stream, cc_uint8 type, union JValue* value) { + struct JUnion obj; + cc_result res; + + switch (type) { + case JFIELD_I8: + case JFIELD_BOOL: + return stream->ReadU8(stream, &value->U8); + case JFIELD_F32: + case JFIELD_I32: + return Stream_ReadU32_BE(stream, &value->U32); + case JFIELD_F64: + case JFIELD_I64: + return stream->Skip(stream, 8); /* (8) data */ + case JFIELD_OBJECT: + return Java_ReadObject(stream, &obj); + + case JFIELD_ARRAY: + if ((res = Java_ReadObject(stream, &obj))) return res; + value->Array.Size = 0; + value->Array.Ptr = NULL; + + /* Array is a byte array */ + /* NOTE: This can technically leak memory if this array is discarded, however */ + /* so far the only observed byte array in .dat files is the map blocks anyways */ + if (obj.Type == TC_ARRAY && obj.Value.Array.Desc->ClassName[1] == JFIELD_I8) { + value->Array.Size = obj.Value.Array.Size; + value->Array.Ptr = obj.Value.Array.Data; + } + return 0; + } + return JAVA_ERR_JVALUE_TYPE; +} + +static cc_result Java_ReadClassData(struct Stream* stream, struct JClassDesc* desc) { + struct JFieldDesc* field; + cc_result res; + int i; + + if (!(desc->Flags & SC_SERIALIZABLE)) + return JAVA_ERR_JOBJECT_FLAGS; + + for (i = 0; i < desc->FieldsCount; i++) + { + field = &desc->Fields[i]; + if ((res = Java_ReadValue(stream, field->Type, &field->Value))) return res; + } + + if (desc->Flags & SC_WRITE_METHOD) + return Java_SkipAnnotation(stream); + return 0; +} + +static cc_result Java_ReadNewString(struct Stream* stream, struct JUnion* object) { + Java_AddReference(); + return Java_ReadString(stream, object->Value.String); +} + +static cc_result Java_ReadNewObject(struct Stream* stream, struct JUnion* object) { + struct JClassDesc* head; + cc_result res; + if ((res = Java_ReadClassDesc(stream, &object->Value.Object))) return res; + Java_AddReference(); + + /* Linked list of classes, with most superclass fist as head */ + head = object->Value.Object; head->tmp = NULL; + while (head->SuperClass) { + head->SuperClass->tmp = head; + head = head->SuperClass; + } + + /* Class data is read with most superclass first */ + while (head) { + if ((res = Java_ReadClassData(stream, head))) return res; + head = head->tmp; + } + return 0; +} + +static cc_result Java_ReadNewArray(struct Stream* stream, struct JUnion* object) { + struct JArray* array = &object->Value.Array; + union JValue value; + cc_uint32 count; + cc_uint8 type; + cc_result res; + int i; + + if ((res = Java_ReadClassDesc(stream, &array->Desc))) return res; + if ((res = Stream_ReadU32_BE(stream, &count))) return res; + type = array->Desc->ClassName[1]; + Java_AddReference(); + + if (type != JFIELD_I8) { + /* Not a byte array, so just discard the unnecessary values */ + for (i = 0; i < count; i++) + { + if ((res = Java_ReadValue(stream, type, &value))) return res; + } + return 0; + } + + array->Size = count; + array->Data = (cc_uint8*)Mem_TryAlloc(count, 1); + + if (!array->Data) return ERR_OUT_OF_MEMORY; + res = Stream_Read(stream, array->Data, count); + if (res) { Mem_Free(array->Data); } + return res; +} + +static cc_result Java_ReadObjectData(struct Stream* stream, struct JUnion* object) { + cc_uint32 reference; + switch (object->Type) + { + case TC_STRING: return Java_ReadNewString(stream, object); + case TC_NULL: return 0; + case TC_REFERENCE: return Stream_ReadU32_BE(stream, &reference); + case TC_OBJECT: return Java_ReadNewObject(stream, object); + case TC_ARRAY: return Java_ReadNewArray(stream, object); + } + return JAVA_ERR_INVALID_TYPECODE; +} + +static cc_result Java_ReadObject(struct Stream* stream, struct JUnion* object) { + cc_result res; + if ((res = stream->ReadU8(stream, &object->Type))) return res; + return Java_ReadObjectData(stream, object); +} + +static int Java_I32(struct JFieldDesc* field) { + if (field->Type != JFIELD_I32) Logger_Abort("Field type must be Int32"); + return field->Value.I32; +} + + +/*########################################################################################################################* +*-------------------------------------------------Minecraft .dat format---------------------------------------------------* +*#########################################################################################################################*/ +/* Minecraft Classic used 3 different GZIP compressed binary map formats throughout its various versions +Preclassic - Classic 0.12: + U8* "Blocks" (256x64x256 array) +Classic 0.13: + U32 "Identifier" (must be 0x271BB788) + U8 "Version" (must be 1) + STR "Name" (ignored) + STR "Author" (ignored) + U64 "Creation" (ignored) + U16 "Width" + U16 "Length" + U16 "Height" + U8* "Blocks" +Classic 0.15 to Classic 0.30: + U32 "Identifier" (must be 0x271BB788) + U8 "Version" (must be 2) + VAR "Level" (Java serialised level object instance) +}*/ + +static void Dat_Format0And1(void) { + /* Formats 0 and 1 don't store spawn position, so use default of map centre */ + spawn_point = NULL; + + /* Similiar env to how it appears in preclassic - 0.13 classic client */ + Env.CloudsHeight = -30000; + Env.SkyCol = PackedCol_Make(0x7F, 0xCC, 0xFF, 0xFF); + Env.FogCol = PackedCol_Make(0x7F, 0xCC, 0xFF, 0xFF); +} + +static cc_result Dat_LoadFormat0(struct Stream* stream) { + Dat_Format0And1(); + /* Similiar env to how it appears in preclassic client */ + Env.EdgeBlock = BLOCK_AIR; + Env.SidesBlock = BLOCK_AIR; + + /* Map 'format' is just the 256x64x256 blocks of the level */ + World.Width = 256; + World.Height = 64; + World.Length = 256; + + #define PC_VOLUME (256 * 64 * 256) + World.Volume = PC_VOLUME; + World.Blocks = (BlockRaw*)Mem_TryAlloc(PC_VOLUME, 1); + if (!World.Blocks) return ERR_OUT_OF_MEMORY; + + /* First 5 bytes already read earlier as .dat header */ + Mem_Set(World.Blocks, BLOCK_STONE, 5); + return Stream_Read(stream, World.Blocks + 5, PC_VOLUME - 5); +} + +static cc_result Dat_LoadFormat1(struct Stream* stream) { + cc_uint8 level_name[JNAME_SIZE]; + cc_uint8 level_author[JNAME_SIZE]; + cc_uint8 header[8 + 2 + 2 + 2]; + cc_result res; + + Dat_Format0And1(); + if ((res = Java_ReadString(stream, level_name))) return res; + if ((res = Java_ReadString(stream, level_author))) return res; + if ((res = Stream_Read(stream, header, sizeof(header)))) return res; + + /* bytes 0-8 = created timestamp (currentTimeMillis) */ + World.Width = Stream_GetU16_BE(header + 8); + World.Length = Stream_GetU16_BE(header + 10); + World.Height = Stream_GetU16_BE(header + 12); + return Map_ReadBlocks(stream); +} + +static cc_result Dat_LoadFormat2(struct Stream* stream) { + struct JClassDesc classes[CLASS_CAPACITY]; + cc_uint8 header[2 + 2]; + struct JUnion obj; + struct JClassDesc* desc; + struct JFieldDesc* field; + cc_string fieldName; + cc_result res; + int i; + if ((res = Stream_Read(stream, header, sizeof(header)))) return res; + + /* Reset state for Java Serialisation */ + class_cache = classes; + class_count = 0; + reference_id = 0x7E0000; + + /* Java seralisation headers */ + if (Stream_GetU16_BE(header + 0) != 0xACED) return DAT_ERR_JIDENTIFIER; + if (Stream_GetU16_BE(header + 2) != 0x0005) return DAT_ERR_JVERSION; + + if ((res = Java_ReadObject(stream, &obj))) return res; + if (obj.Type != TC_OBJECT) return DAT_ERR_ROOT_OBJECT; + desc = obj.Value.Object; + + for (i = 0; i < desc->FieldsCount; i++) + { + field = &desc->Fields[i]; + fieldName = String_FromRaw((char*)field->FieldName, JNAME_SIZE); + + if (String_CaselessEqualsConst(&fieldName, "width")) { + World.Width = Java_I32(field); + } else if (String_CaselessEqualsConst(&fieldName, "height")) { + World.Length = Java_I32(field); + } else if (String_CaselessEqualsConst(&fieldName, "depth")) { + World.Height = Java_I32(field); + } else if (String_CaselessEqualsConst(&fieldName, "blocks")) { + if (field->Type != JFIELD_ARRAY) Logger_Abort("Blocks field must be Array"); + World.Blocks = field->Value.Array.Ptr; + World.Volume = field->Value.Array.Size; + } else if (String_CaselessEqualsConst(&fieldName, "xSpawn")) { + spawn_point->pos.x = (float)Java_I32(field); + spawn_point->flags = LU_HAS_POS; + } else if (String_CaselessEqualsConst(&fieldName, "ySpawn")) { + spawn_point->pos.y = (float)Java_I32(field); + spawn_point->flags = LU_HAS_POS; + } else if (String_CaselessEqualsConst(&fieldName, "zSpawn")) { + spawn_point->pos.z = (float)Java_I32(field); + spawn_point->flags = LU_HAS_POS; + } + } + return 0; +} + +/* Imports a world from a .dat classic map file */ +/* Used by Minecraft Classic/WoM client */ +static cc_result Dat_Load(struct Stream* stream) { + cc_uint8 header[4 + 1]; + cc_uint32 signature; + cc_result res; + + struct Stream compStream; + struct InflateState state; + Inflate_MakeStream2(&compStream, &state, stream); + if ((res = Map_SkipGZipHeader(stream))) return res; + if ((res = Stream_Read(&compStream, header, sizeof(header)))) return res; + + signature = Stream_GetU32_BE(header + 0); + switch (signature) + { + /* Classic map format signature */ + case 0x271BB788: break; + /* Not an actual signature, but 99% of preclassic */ + /* to classic 0.12 maps start with these 4 bytes */ + case 0x01010101: return Dat_LoadFormat0(&compStream); + /* Bogus .dat file */ + default: return DAT_ERR_IDENTIFIER; + } + + /* Format version */ + switch (header[4]) + { + /* Format version 1 = classic 0.13 */ + case 0x01: return Dat_LoadFormat1(&compStream); + /* Format version 2 = classic 0.15 to 0.30 */ + case 0x02: return Dat_LoadFormat2(&compStream); + /* Bogus .dat file */ + default: return DAT_ERR_VERSION; + } +} + + +/*########################################################################################################################* +*-----------------------------------------------------MCLevel format------------------------------------------------------* +*#########################################################################################################################*/ +/* MCLevel is a NBT tag based map format used by Minecraft Indev. Tags not listed below are discarded. +COMPOUND "MinecraftLevel" { + COMPOUND "Map" { + I16 "Width", "Height", "Length" + I16 "Spawn" [3] + U8* "Blocks" + } + COMPOUND "Environment" { + I32 "SkyColor" + I32 "FogColor" + I32 "CloudColor" + I16 "CloudHeight" + } +}*/ +static int mcl_edgeHeight, mcl_sidesHeight; + +static void MCLevel_ParseMap(struct NbtTag* tag) { + if (IsTag(tag, "width")) { World.Width = NbtTag_U16(tag); return; } + if (IsTag(tag, "height")) { World.Height = NbtTag_U16(tag); return; } + if (IsTag(tag, "length")) { World.Length = NbtTag_U16(tag); return; } + + if (IsTag(tag, "blocks")) { + World.Volume = tag->dataSize; + World.Blocks = Nbt_TakeArray(tag, ".mclevel map blocks"); + } +} + +static PackedCol MCLevel_ParseColor(struct NbtTag* tag) { + int RGB = NbtTag_I32(tag); + return PackedCol_Make(RGB >> 16, RGB >> 8, RGB, 255); +} + +static void MCLevel_ParseEnvironment(struct NbtTag* tag) { + if (IsTag(tag, "SkyColor")) { + Env.SkyCol = MCLevel_ParseColor(tag); + } else if (IsTag(tag, "FogColor")) { + Env.FogCol = MCLevel_ParseColor(tag); + } else if (IsTag(tag, "CloudColor")) { + Env.CloudsCol = MCLevel_ParseColor(tag); + } else if (IsTag(tag, "CloudHeight")) { + Env.CloudsHeight = NbtTag_U16(tag); + } else if (IsTag(tag, "SurroundingGroundType")) { + Env.SidesBlock = NbtTag_U8(tag); + /* TODO need to explore this fully */ + if (Env.SidesBlock == BLOCK_GRASS) Env.SidesBlock = BLOCK_DIRT; + } else if (IsTag(tag, "SurroundingWaterType")) { + Env.EdgeBlock = NbtTag_U8(tag); + } else if (IsTag(tag, "SurroundingGroundHeight")) { + mcl_sidesHeight = NbtTag_U16(tag); + } else if (IsTag(tag, "SurroundingWaterHeight")) { + mcl_edgeHeight = NbtTag_U16(tag); + } + /* TODO: SkyBrightness */ +} + + +static void MCLevel_Callback_2(struct NbtTag* tag) { + struct NbtTag* group = tag->parent; + if (IsTag(group, "Map")) { + MCLevel_ParseMap(tag); + } else if (IsTag(group, "Environment")) { + MCLevel_ParseEnvironment(tag); + } +} + +static void MCLevel_Callback_3(struct NbtTag* tag) { + struct NbtTag* group = tag->parent->parent; + struct NbtTag* field = tag->parent; + + if (IsTag(group, "Map") && IsTag(field, "spawn")) { + cc_int16 value = NbtTag_I16(tag); + spawn_point->flags = LU_HAS_POS; + + if (tag->listIndex == 0) spawn_point->pos.x = value; + if (tag->listIndex == 1) spawn_point->pos.y = value - 1.0f; + if (tag->listIndex == 2) spawn_point->pos.z = value; + } +} + +static void MCLevel_Callback(struct NbtTag* tag) { + struct NbtTag* tmp = tag->parent; + int depth = 0; + while (tmp) { depth++; tmp = tmp->parent; } + + switch (depth) { + case 2: MCLevel_Callback_2(tag); return; + case 3: MCLevel_Callback_3(tag); return; + } + /* MinecraftLevel -> Map/Environment -> [value] + 0 1 2 */ +} + +/* Imports a world from a .mclevel NBT map file */ +/* Used by Minecraft Indev client */ +static cc_result MCLevel_Load(struct Stream* stream) { + cc_result res = Nbt_Read(stream, MCLevel_Callback); + + Env.EdgeHeight = mcl_edgeHeight; + Env.SidesOffset = mcl_sidesHeight - mcl_edgeHeight; + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------ClassicWorld export----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint8* Cw_WriteColor(cc_uint8* data, const char* name, PackedCol color) { + data = Nbt_WriteDict(data, name); + { + data = Nbt_WriteUInt16(data, "R", PackedCol_R(color)); + data = Nbt_WriteUInt16(data, "G", PackedCol_G(color)); + data = Nbt_WriteUInt16(data, "B", PackedCol_B(color)); + } *data++ = NBT_END; + + return data; +} + +static const cc_uint8 cw_end[4] = { + NBT_END, + NBT_END, + NBT_END, +NBT_END, +}; + +static cc_result Cw_WriteBockDef(struct Stream* stream, int b) { + cc_uint8 buffer[1024]; + char nameBuffer[10]; + cc_uint8* cur; + cc_string name; + cc_bool sprite = Blocks.Draw[b] == DRAW_SPRITE; + TextureLoc tex; + cc_uint8 fog; + PackedCol col; + Vec3 minBB, maxBB; + + /* Hacky unique tag name for each by using hex of block */ + String_InitArray_NT(name, nameBuffer); + String_AppendConst(&name, "Block"); + String_AppendHex(&name, b >> 8); + String_AppendHex(&name, b); + nameBuffer[9] = '\0'; + + cur = buffer; + cur = Nbt_WriteDict(cur, nameBuffer); + { + cur = Nbt_WriteUInt8(cur, "ID", b); + /* It would be have been better to just change ID to be a I16 */ + /* Unfortunately this isn't backwards compatible with ClassicalSharp */ + cur = Nbt_WriteUInt16(cur, "ID2", b); + cur = Nbt_WriteUInt8(cur, "CollideType", Blocks.Collide[b]); + cur = Nbt_WriteFloat(cur, "Speed", Blocks.SpeedMultiplier[b]); + + /* Originally only up to 256 textures were supported, which used up 6 bytes total */ + /* Later, support for more textures was added, which requires 2 bytes per texture */ + /* For backwards compatibility, the lower byte of each texture is */ + /* written into first 6 bytes, then higher byte into next 6 bytes (ugly hack) */ + cur = Nbt_WriteArray(cur, "Textures", 12); + tex = Block_Tex(b, FACE_YMAX); cur[0] = (cc_uint8)tex; cur[ 6] = (cc_uint8)(tex >> 8); + tex = Block_Tex(b, FACE_YMIN); cur[1] = (cc_uint8)tex; cur[ 7] = (cc_uint8)(tex >> 8); + tex = Block_Tex(b, FACE_XMIN); cur[2] = (cc_uint8)tex; cur[ 8] = (cc_uint8)(tex >> 8); + tex = Block_Tex(b, FACE_XMAX); cur[3] = (cc_uint8)tex; cur[ 9] = (cc_uint8)(tex >> 8); + tex = Block_Tex(b, FACE_ZMIN); cur[4] = (cc_uint8)tex; cur[10] = (cc_uint8)(tex >> 8); + tex = Block_Tex(b, FACE_ZMAX); cur[5] = (cc_uint8)tex; cur[11] = (cc_uint8)(tex >> 8); + cur += 12; + + cur = Nbt_WriteUInt8(cur, "TransmitsLight", Blocks.BlocksLight[b] ? 0 : 1); + cur = Nbt_WriteUInt8(cur, "WalkSound", Blocks.DigSounds[b]); + cur = Nbt_WriteUInt8(cur, "FullBright", Block_WriteFullBright(Blocks.Brightness[b])); + cur = Nbt_WriteUInt8(cur, "Shape", sprite ? 0 : (cc_uint8)(Blocks.MaxBB[b].y * 16)); + cur = Nbt_WriteUInt8(cur, "BlockDraw", sprite ? Blocks.SpriteOffset[b] : Blocks.Draw[b]); + + cur = Nbt_WriteArray(cur, "Fog", 4); + fog = (cc_uint8)(128 * Blocks.FogDensity[b] - 1); + col = Blocks.FogCol[b]; + cur[0] = Blocks.FogDensity[b] ? fog : 0xFF; /* write 0xFF instead of 0 for backwards compatibility */ + cur[1] = PackedCol_R(col); cur[2] = PackedCol_G(col); cur[3] = PackedCol_B(col); + cur += 4; + + cur = Nbt_WriteArray(cur, "Coords", 6); + minBB = Blocks.MinBB[b]; maxBB = Blocks.MaxBB[b]; + cur[0] = (cc_uint8)(minBB.x * 16); cur[1] = (cc_uint8)(minBB.y * 16); cur[2] = (cc_uint8)(minBB.z * 16); + cur[3] = (cc_uint8)(maxBB.x * 16); cur[4] = (cc_uint8)(maxBB.y * 16); cur[5] = (cc_uint8)(maxBB.z * 16); + cur += 6; + + name = Block_UNSAFE_GetName(b); + cur = Nbt_WriteString(cur, "Name", &name); + } *cur++ = NBT_END; + + return Stream_Write(stream, buffer, (int)(cur - buffer)); +} + +cc_result Cw_Save(struct Stream* stream) { + struct LocalPlayer* p = Entities.CurPlayer; + cc_uint8 buffer[2048]; + cc_uint8* cur; + cc_result res; + int b; + + cur = buffer; + cur = Nbt_WriteDict(cur, "ClassicWorld"); + cur = Nbt_WriteUInt8(cur, "FormatVersion", 1); + cur = Nbt_WriteArray(cur, "UUID", WORLD_UUID_LEN); Mem_Copy(cur, World.Uuid, WORLD_UUID_LEN); cur += WORLD_UUID_LEN; + cur = Nbt_WriteUInt16(cur, "X", World.Width); + cur = Nbt_WriteUInt16(cur, "Y", World.Height); + cur = Nbt_WriteUInt16(cur, "Z", World.Length); + + cur = Nbt_WriteDict(cur, "MapGenerator"); + { + cur = Nbt_WriteInt32(cur, "Seed", World.Seed); + } *cur++ = NBT_END; + + + /* TODO: Maybe keep real spawn too? */ + cur = Nbt_WriteDict(cur, "Spawn"); + { + cur = Nbt_WriteUInt16(cur, "X", (cc_uint16)p->Base.Position.x); + cur = Nbt_WriteUInt16(cur, "Y", (cc_uint16)p->Base.Position.y); + cur = Nbt_WriteUInt16(cur, "Z", (cc_uint16)p->Base.Position.z); + cur = Nbt_WriteUInt8(cur, "H", Math_Deg2Packed(p->SpawnYaw)); + cur = Nbt_WriteUInt8(cur, "P", Math_Deg2Packed(p->SpawnPitch)); + } *cur++ = NBT_END; + cur = Nbt_WriteArray(cur, "BlockArray", World.Volume); + + if ((res = Stream_Write(stream, buffer, (int)(cur - buffer)))) return res; + if ((res = Stream_Write(stream, World.Blocks, World.Volume))) return res; + +#ifdef EXTENDED_BLOCKS + if (World.Blocks != World.Blocks2) { + cur = buffer; + cur = Nbt_WriteArray(cur, "BlockArray2", World.Volume); + + if ((res = Stream_Write(stream, buffer, (int)(cur - buffer)))) return res; + if ((res = Stream_Write(stream, World.Blocks2, World.Volume))) return res; + } +#endif + + cur = buffer; + cur = Nbt_WriteDict(cur, "Metadata"); + cur = Nbt_WriteDict(cur, "CPE"); + { + cur = Nbt_WriteDict(cur, "ClickDistance"); + { + cur = Nbt_WriteUInt16(cur, "Distance", (cc_uint16)(p->ReachDistance * 32)); + } *cur++ = NBT_END; + + cur = Nbt_WriteDict(cur, "EnvWeatherType"); + { + cur = Nbt_WriteUInt8(cur, "WeatherType", Env.Weather); + } *cur++ = NBT_END; + + cur = Nbt_WriteDict(cur, "EnvColors"); + { + cur = Cw_WriteColor(cur, "Sky", Env.SkyCol); + cur = Cw_WriteColor(cur, "Cloud", Env.CloudsCol); + cur = Cw_WriteColor(cur, "Fog", Env.FogCol); + cur = Cw_WriteColor(cur, "Ambient", Env.ShadowCol); + cur = Cw_WriteColor(cur, "Sunlight", Env.SunCol); + cur = Cw_WriteColor(cur, "Skybox", Env.SkyboxCol); + } *cur++ = NBT_END; + + cur = Nbt_WriteDict(cur, "EnvMapAppearance"); + { + cur = Nbt_WriteUInt8(cur, "SideBlock", (BlockRaw)Env.SidesBlock); + cur = Nbt_WriteUInt8(cur, "EdgeBlock", (BlockRaw)Env.EdgeBlock); + cur = Nbt_WriteUInt16(cur, "SideLevel", Env.EdgeHeight); + cur = Nbt_WriteString(cur, "TextureURL", &TexturePack_Url); + } *cur++ = NBT_END; + + cur = Nbt_WriteDict(cur, "EnvMapAspect"); + { + cur = Nbt_WriteUInt16(cur, "EdgeBlock", Env.EdgeBlock); + cur = Nbt_WriteUInt16(cur, "SideBlock", Env.SidesBlock); + cur = Nbt_WriteInt32(cur, "EdgeHeight", Env.EdgeHeight); + cur = Nbt_WriteInt32(cur, "SidesOffset", Env.SidesOffset); + cur = Nbt_WriteInt32(cur, "CloudsHeight", Env.CloudsHeight); + cur = Nbt_WriteFloat(cur, "CloudsSpeed", Env.CloudsSpeed); + cur = Nbt_WriteFloat(cur, "WeatherSpeed", Env.WeatherSpeed); + cur = Nbt_WriteFloat(cur, "WeatherFade", Env.WeatherFade); + cur = Nbt_WriteUInt8(cur, "ExpFog", (cc_uint8)Env.ExpFog); + cur = Nbt_WriteFloat(cur, "SkyboxHor", Env.SkyboxHorSpeed); + cur = Nbt_WriteFloat(cur, "SkyboxVer", Env.SkyboxVerSpeed); + } *cur++ = NBT_END; + + cur = Nbt_WriteDict(cur, "BlockDefinitions"); + if ((res = Stream_Write(stream, buffer, (int)(cur - buffer)))) return res; + + { + /* Write block definitions in reverse order so that software that only reads byte 'ID' */ + /* still loads correct first 256 block defs when saving a map with over 256 block defs */ + for (b = BLOCK_MAX_DEFINED; b >= 1; b--) { + if (!Block_IsCustomDefined(b)) continue; + if ((res = Cw_WriteBockDef(stream, b))) return res; + } + } + } + return Stream_Write(stream, cw_end, sizeof(cw_end)); +} + + +/*########################################################################################################################* +*---------------------------------------------------Schematic export------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint8 sc_begin[] = { +NBT_DICT, 0,9, 'S','c','h','e','m','a','t','i','c', + NBT_STR, 0,9, 'M','a','t','e','r','i','a','l','s', 0,7, 'C','l','a','s','s','i','c', + NBT_I16, 0,5, 'W','i','d','t','h', 0,0, + NBT_I16, 0,6, 'H','e','i','g','h','t', 0,0, + NBT_I16, 0,6, 'L','e','n','g','t','h', 0,0, + NBT_I8S, 0,6, 'B','l','o','c','k','s', 0,0,0,0, +}; +static cc_uint8 sc_data[] = { + NBT_I8S, 0,4, 'D','a','t','a', 0,0,0,0, +}; +static cc_uint8 sc_end[] = { + NBT_LIST, 0,8, 'E','n','t','i','t','i','e','s', NBT_DICT, 0,0,0,0, + NBT_LIST, 0,12, 'T','i','l','e','E','n','t','i','t','i','e','s', NBT_DICT, 0,0,0,0, +NBT_END, +}; + +cc_result Schematic_Save(struct Stream* stream) { + cc_uint8 tmp[256], chunk[8192] = { 0 }; + cc_result res; + int i; + + Mem_Copy(tmp, sc_begin, sizeof(sc_begin)); + { + Stream_SetU16_BE(&tmp[41], World.Width); + Stream_SetU16_BE(&tmp[52], World.Height); + Stream_SetU16_BE(&tmp[63], World.Length); + Stream_SetU32_BE(&tmp[74], World.Volume); + } + if ((res = Stream_Write(stream, tmp, sizeof(sc_begin)))) return res; + if ((res = Stream_Write(stream, World.Blocks, World.Volume))) return res; + + Mem_Copy(tmp, sc_data, sizeof(sc_data)); + { + Stream_SetU32_BE(&tmp[7], World.Volume); + } + if ((res = Stream_Write(stream, tmp, sizeof(sc_data)))) return res; + + for (i = 0; i < World.Volume; i += sizeof(chunk)) { + int count = World.Volume - i; count = min(count, sizeof(chunk)); + if ((res = Stream_Write(stream, chunk, count))) return res; + } + return Stream_Write(stream, sc_end, sizeof(sc_end)); +} + + +/*########################################################################################################################* +*------------------------------------------------------Dat export---------------------------------------------------------* +*#########################################################################################################################*/ +static const struct JField { + cc_uint8 type, isFloat; + const char* name; + void* value; +} level_fields[] = { + { JFIELD_I32, false, "width", &World.Width }, + { JFIELD_I32, false, "depth", &World.Height }, + { JFIELD_I32, false, "height", &World.Length }, + { JFIELD_I32, true, "xSpawn", &LocalPlayer_Instances[0].Base.Position.x }, + { JFIELD_I32, true, "ySpawn", &LocalPlayer_Instances[0].Base.Position.y }, + { JFIELD_I32, true, "zSpawn", &LocalPlayer_Instances[0].Base.Position.z }, + { JFIELD_ARRAY,0, "blocks" } + /* TODO classic only blocks */ +}; + +static int WriteJavaString(cc_uint8* dst, const char* value) { + int length = String_Length(value); + dst[0] = 0; + dst[1] = length; + Mem_Copy(dst + 2, value, length); + return length; +} + +static cc_result WriteClassDesc(struct Stream* stream, cc_uint8 typecode, const char* klass, + int numFields, const struct JField* fields) { + cc_uint8 header[256] = { 0 }; + static const cc_uint8 footer[] = { + TC_ENDBLOCKDATA, /* classAnnotations */ + TC_NULL /* superClassDesc */ + }; + int i, length; + cc_result res; + + header[0] = typecode; + header[1] = TC_CLASSDESC; + length = WriteJavaString(header + 2, klass); + header[4 + length + 8] = SC_SERIALIZABLE; + header[4 + length + 9] = 0; + header[4 + length + 10] = numFields; + + if ((res = Stream_Write(stream, header, 15 + length))) return res; + + for (i = 0; i < numFields; i++) + { + header[0] = fields[i].type; + length = WriteJavaString(header + 1, fields[i].name); + + if (fields[i].type == JFIELD_ARRAY) { + header[3 + length + 0] = TC_STRING; + WriteJavaString(&header[3 + length + 1], "[B"); + length += 5; + } + if ((res = Stream_Write(stream, header, 3 + length))) return res; + } + + if ((res = Stream_Write(stream, footer, sizeof(footer)))) return res; + return 0; +} + +static const cc_uint8 cpe_fallback[] = { + BLOCK_SLAB, BLOCK_BROWN_SHROOM, BLOCK_SAND, BLOCK_AIR, BLOCK_STILL_LAVA, BLOCK_PINK, + BLOCK_GREEN, BLOCK_DIRT, BLOCK_BLUE, BLOCK_CYAN, BLOCK_GLASS, BLOCK_IRON, BLOCK_OBSIDIAN, BLOCK_WHITE, + BLOCK_WOOD, BLOCK_STONE +}; + +#define DAT_BUFFER_SIZE (64 * 1024) +static cc_result WriteLevelBlocks(struct Stream* stream) { + cc_uint8 buffer[DAT_BUFFER_SIZE]; + int i, bIndex = 0; + cc_result res; + BlockID b; + + for (i = 0; i < World.Volume; i++) + { + b = World_GetRawBlock(i); + /* TODO: Better fallback decision (e.g. air if custom block is 'gas' type) */ + if (b > BLOCK_STONE_BRICK) b = BLOCK_STONE; + /* TODO: Move to GameVersion.c and account for game version */ + if (b > BLOCK_OBSIDIAN) b = cpe_fallback[b - BLOCK_COBBLE_SLAB]; + + buffer[bIndex] = (cc_uint8)b; + bIndex++; + if (bIndex < DAT_BUFFER_SIZE) continue; + + if ((res = Stream_Write(stream, buffer, DAT_BUFFER_SIZE))) return res; + bIndex = 0; + } + + if (bIndex == 0) return 0; + return Stream_Write(stream, buffer, bIndex); +} + +cc_result Dat_Save(struct Stream* stream) { + static const cc_uint8 header[] = { + 0x27,0x1B,0xB7,0x88, 0x02, /* DAT signature + version */ + 0xAC,0xED, 0x00,0x05 /* JSF signature + version */ + }; + const struct JField* field; + cc_uint8 tmp[4]; + cc_result res; + int i, value; + + if ((res = Stream_Write(stream, header, sizeof(header)))) return res; + if ((res = WriteClassDesc(stream, TC_OBJECT, "com.mojang.minecraft.level.Level", + Array_Elems(level_fields), level_fields))) return res; + + /* Write field values */ + for (i = 0; i < Array_Elems(level_fields); i++) + { + field = &level_fields[i]; + + if (field->type == JFIELD_I32) { + value = field->isFloat ? *((float*)field->value) : *((int*)field->value); + Stream_SetU32_BE(tmp, value); + if ((res = Stream_Write(stream, tmp, 4))) return res; + } else { + if ((res = WriteClassDesc(stream, TC_ARRAY, "[B", 0, NULL))) return res; + Stream_SetU32_BE(tmp, World.Volume); + if ((res = Stream_Write(stream, tmp, 4))) return res; + if ((res = WriteLevelBlocks(stream))) return res; + } + } + return 0; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Formats component-------------------------------------------------* +*#########################################################################################################################*/ +static struct MapImporter cw_imp = { ".cw", Cw_Load }; +static struct MapImporter dat_imp = { ".dat", Dat_Load }; +static struct MapImporter lvl_imp = { ".lvl", Lvl_Load }; +static struct MapImporter mine_imp = { ".mine", Dat_Load }; +static struct MapImporter fcm_imp = { ".fcm", Fcm_Load }; +static struct MapImporter mclvl_imp = { ".mclevel", MCLevel_Load }; + +static void OnInit(void) { + MapImporter_Register(&cw_imp); + MapImporter_Register(&dat_imp); + MapImporter_Register(&lvl_imp); + MapImporter_Register(&mine_imp); + MapImporter_Register(&fcm_imp); + MapImporter_Register(&mclvl_imp); +} + +static void OnFree(void) { + imp_head = NULL; +} +#else +/* No point including map format code when can't save/load maps anyways */ +struct MapImporter* MapImporter_Find(const cc_string* path) { return NULL; } +cc_result Map_LoadFrom(const cc_string* path) { return ERR_NOT_SUPPORTED; } + +cc_result Cw_Save(struct Stream* stream) { return ERR_NOT_SUPPORTED; } +cc_result Dat_Save(struct Stream* stream) { return ERR_NOT_SUPPORTED; } +cc_result Schematic_Save(struct Stream* stream) { return ERR_NOT_SUPPORTED; } + +static void OnInit(void) { } +static void OnFree(void) { } +#endif + +struct IGameComponent Formats_Component = { + OnInit, /* Init */ + OnFree /* Free */ +}; diff --git a/src/Formats.h b/src/Formats.h new file mode 100644 index 0000000..e1aa56a --- /dev/null +++ b/src/Formats.h @@ -0,0 +1,39 @@ +#ifndef CC_MAPFORMATS_H +#define CC_MAPFORMATS_H +#include "Core.h" +/* Imports/exports a world and associated metadata from/to a particular map file format. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct Stream; +struct IGameComponent; +extern struct IGameComponent Formats_Component; + +/* Imports a world encoded in a particular map file format */ +typedef cc_result (*MapImportFunc)(struct Stream* stream); +struct MapImporter; +/* Reads/Loads world data (and potentially metadata) encoded in a particular format */ +struct MapImporter { + const char* fileExt; /* File extension of the map format */ + MapImportFunc import; /* Function that imports the encoded data */ + struct MapImporter* next; /* Next importer in linked-list of map importers */ +}; + +/* Adds the given importer to the list of map importers */ +CC_API void MapImporter_Register(struct MapImporter* imp); +/* Attempts to find a suitable map importer based on filename */ +/* Returns NULL if no match found */ +CC_API struct MapImporter* MapImporter_Find(const cc_string* path); +/* Attempts to import a map from the given file */ +CC_API cc_result Map_LoadFrom(const cc_string* path); + +/* Exports a world to a .cw ClassicWorld map file. */ +/* Compatible with ClassiCube/ClassicalSharp */ +cc_result Cw_Save(struct Stream* stream); +/* Exports a world to a .schematic Schematic map file */ +/* Used by MCEdit and other tools */ +cc_result Schematic_Save(struct Stream* stream); +/* Exports a world to a .dat Classic map file */ +/* Used by MineCraft Classic */ +cc_result Dat_Save(struct Stream* stream); +#endif diff --git a/src/Funcs.h b/src/Funcs.h new file mode 100644 index 0000000..3ad535b --- /dev/null +++ b/src/Funcs.h @@ -0,0 +1,52 @@ +#ifndef CC_FUNCS_H +#define CC_FUNCS_H +#include "Core.h" +/* +Simple function implementations + NOTE: doing min(x++, y) etc will increment x twice! +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +#define min(x, y) ((x) < (y) ? (x) : (y)) +#define max(x, y) ((x) > (y) ? (x) : (y)) +#define Array_Elems(arr) (sizeof(arr) / sizeof(arr[0])) +union IntAndFloat { float f; cc_int32 i; cc_uint32 u; }; + +#define QuickSort_Swap_Maybe()\ +if (i <= j) {\ + key = keys[i]; keys[i] = keys[j]; keys[j] = key;\ + i++; j--;\ +} + +#define QuickSort_Swap_KV_Maybe()\ +if (i <= j) {\ + key = keys[i]; keys[i] = keys[j]; keys[j] = key;\ + value = values[i]; values[i] = values[j]; values[j] = value;\ + i++; j--;\ +} + +#define QuickSort_Recurse(quickSort)\ +if (j - left <= right - i) {\ + if (left < j) { quickSort(left, j); }\ + left = i;\ +} else {\ + if (i < right) { quickSort(i, right); }\ + right = j;\ +} + +#define LinkedList_Append(item, head, tail)\ +if (!head) { head = item; } else { tail->next = item; }\ +tail = item;\ +item->next = NULL; + +#define LinkedList_Remove(item, cur, head, tail)\ +cur = head; \ +if (head == item) head = item->next;\ +\ +while (cur) {\ + if (cur->next == item) cur->next = item->next; \ + \ + tail = cur;\ + cur = cur->next;\ +} +#endif diff --git a/src/Game.c b/src/Game.c new file mode 100644 index 0000000..185eec4 --- /dev/null +++ b/src/Game.c @@ -0,0 +1,796 @@ +#include "Game.h" +#include "Block.h" +#include "World.h" +#include "Lighting.h" +#include "MapRenderer.h" +#include "Graphics.h" +#include "Camera.h" +#include "Options.h" +#include "Funcs.h" +#include "ExtMath.h" +#include "Gui.h" +#include "Window.h" +#include "Event.h" +#include "Utils.h" +#include "Logger.h" +#include "Entity.h" +#include "Chat.h" +#include "Commands.h" +#include "Drawer2D.h" +#include "Model.h" +#include "Particle.h" +#include "Http.h" +#include "Inventory.h" +#include "Input.h" +#include "Server.h" +#include "TexturePack.h" +#include "Screens.h" +#include "SelectionBox.h" +#include "AxisLinesRenderer.h" +#include "EnvRenderer.h" +#include "HeldBlockRenderer.h" +#include "SelOutlineRenderer.h" +#include "Menus.h" +#include "Audio.h" +#include "Stream.h" +#include "Builder.h" +#include "Protocol.h" +#include "Picking.h" +#include "Animations.h" +#include "SystemFonts.h" +#include "Formats.h" +#include "EntityRenderers.h" + +struct _GameData Game; +cc_uint64 Game_FrameStart; +cc_bool Game_UseCPEBlocks; + +struct RayTracer Game_SelectedPos; +int Game_ViewDistance = DEFAULT_VIEWDIST; +int Game_UserViewDistance = DEFAULT_VIEWDIST; +int Game_MaxViewDistance = DEFAULT_MAX_VIEWDIST; + +int Game_FpsLimit, Game_Vertices; +cc_bool Game_SimpleArmsAnim; +static cc_bool gameRunning; + +cc_bool Game_ClassicMode, Game_ClassicHacks; +cc_bool Game_AllowCustomBlocks; +cc_bool Game_AllowServerTextures; +cc_bool Game_Anaglyph3D; + +cc_bool Game_ViewBobbing, Game_HideGui; +cc_bool Game_BreakableLiquids, Game_ScreenshotRequested; +struct GameVersion Game_Version; + +static char usernameBuffer[STRING_SIZE]; +static char mppassBuffer[STRING_SIZE]; +cc_string Game_Username = String_FromArray(usernameBuffer); +cc_string Game_Mppass = String_FromArray(mppassBuffer); +#ifdef CC_BUILD_SPLITSCREEN +int Game_NumLocalPlayers = 1; +#endif + +const char* const FpsLimit_Names[FPS_LIMIT_COUNT] = { + "LimitVSync", "Limit30FPS", "Limit60FPS", "Limit120FPS", "Limit144FPS", "LimitNone", +}; + +static struct IGameComponent* comps_head; +static struct IGameComponent* comps_tail; +void Game_AddComponent(struct IGameComponent* comp) { + LinkedList_Append(comp, comps_head, comps_tail); +} + +#define TASKS_DEF_ELEMS 6 +static struct ScheduledTask defaultTasks[TASKS_DEF_ELEMS]; +static int tasksCapacity = TASKS_DEF_ELEMS, tasksCount, entTaskI; +static struct ScheduledTask* tasks = defaultTasks; + +int ScheduledTask_Add(double interval, ScheduledTaskCallback callback) { + struct ScheduledTask task; + task.accumulator = 0.0; + task.interval = interval; + task.Callback = callback; + + if (tasksCount == tasksCapacity) { + Utils_Resize((void**)&tasks, &tasksCapacity, + sizeof(struct ScheduledTask), TASKS_DEF_ELEMS, TASKS_DEF_ELEMS); + } + + tasks[tasksCount++] = task; + return tasksCount - 1; +} + + +void Game_ToggleFullscreen(void) { + int state = Window_GetWindowState(); + cc_result res; + + if (state == WINDOW_STATE_FULLSCREEN) { + res = Window_ExitFullscreen(); + if (res) Logger_SysWarn(res, "leaving fullscreen"); + } else { + res = Window_EnterFullscreen(); + if (res) Logger_SysWarn(res, "going fullscreen"); + } +} + +static void CycleViewDistanceForwards(const short* viewDists, int count) { + int i, dist; + for (i = 0; i < count; i++) { + dist = viewDists[i]; + + if (dist > Game_UserViewDistance) { + Game_UserSetViewDistance(dist); return; + } + } + Game_UserSetViewDistance(viewDists[0]); +} + +static void CycleViewDistanceBackwards(const short* viewDists, int count) { + int i, dist; + for (i = count - 1; i >= 0; i--) { + dist = viewDists[i]; + + if (dist < Game_UserViewDistance) { + Game_UserSetViewDistance(dist); return; + } + } + Game_UserSetViewDistance(viewDists[count - 1]); +} + +static const short normalDists[] = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 }; +static const short classicDists[] = { 8, 32, 128, 512 }; +void Game_CycleViewDistance(void) { + const short* dists = Gui.ClassicMenu ? classicDists : normalDists; + int count = Gui.ClassicMenu ? Array_Elems(classicDists) : Array_Elems(normalDists); + + if (Input_IsShiftPressed()) { + CycleViewDistanceBackwards(dists, count); + } else { + CycleViewDistanceForwards(dists, count); + } +} + +cc_bool Game_ReduceVRAM(void) { + if (Game_UserViewDistance <= 16) return false; + Game_UserViewDistance /= 2; + Game_UserViewDistance = max(16, Game_UserViewDistance); + + MapRenderer_Refresh(); + Game_SetViewDistance(Game_UserViewDistance); + Chat_AddRaw("&cOut of VRAM! Halving view distance.."); + return true; +} + + +void Game_SetViewDistance(int distance) { + distance = min(distance, Game_MaxViewDistance); + if (distance == Game_ViewDistance) return; + Game_ViewDistance = distance; + + Event_RaiseVoid(&GfxEvents.ViewDistanceChanged); + Camera_UpdateProjection(); +} + +void Game_UserSetViewDistance(int distance) { + Game_UserViewDistance = distance; + Options_SetInt(OPT_VIEW_DISTANCE, distance); + Game_SetViewDistance(distance); +} + +void Game_Disconnect(const cc_string* title, const cc_string* reason) { + Event_RaiseVoid(&NetEvents.Disconnected); + Game_Reset(); + DisconnectScreen_Show(title, reason); +} + +void Game_Reset(void) { + struct IGameComponent* comp; + World_NewMap(); + + for (comp = comps_head; comp; comp = comp->next) { + if (comp->Reset) comp->Reset(); + } +} + +void Game_UpdateBlock(int x, int y, int z, BlockID block) { + BlockID old = World_GetBlock(x, y, z); + World_SetBlock(x, y, z, block); + + if (Weather_Heightmap) { + EnvRenderer_OnBlockChanged(x, y, z, old, block); + } + Lighting.OnBlockChanged(x, y, z, old, block); + MapRenderer_OnBlockChanged(x, y, z, block); +} + +void Game_ChangeBlock(int x, int y, int z, BlockID block) { + BlockID old = World_GetBlock(x, y, z); + Game_UpdateBlock(x, y, z, block); + Server.SendBlock(x, y, z, old, block); +} + +cc_bool Game_CanPick(BlockID block) { + if (Blocks.Draw[block] == DRAW_GAS) return false; + if (Blocks.Draw[block] == DRAW_SPRITE) return true; + return Blocks.Collide[block] != COLLIDE_LIQUID || Game_BreakableLiquids; +} + +cc_bool Game_UpdateTexture(GfxResourceID* texId, struct Stream* src, const cc_string* file, + cc_uint8* skinType, int* heightDivisor) { + struct Bitmap bmp; + cc_bool success; + cc_result res; + + res = Png_Decode(&bmp, src); + if (res) { Logger_SysWarn2(res, "decoding", file); } + + /* E.g. gui.png only need top half of the texture loaded */ + if (heightDivisor && bmp.height >= *heightDivisor) + bmp.height /= *heightDivisor; + + success = !res && Game_ValidateBitmap(file, &bmp); + if (success) { + if (skinType) { *skinType = Utils_CalcSkinType(&bmp); } + Gfx_RecreateTexture(texId, &bmp, TEXTURE_FLAG_MANAGED, false); + } + + Mem_Free(bmp.scan0); + return success; +} + +cc_bool Game_ValidateBitmap(const cc_string* file, struct Bitmap* bmp) { + int maxWidth = Gfx.MaxTexWidth, maxHeight = Gfx.MaxTexHeight; + float texSize, maxSize; + + if (!bmp->scan0) { + Chat_Add1("&cError loading %s from the texture pack.", file); + return false; + } + + if (bmp->width > maxWidth || bmp->height > maxHeight) { + Chat_Add1("&cUnable to use %s from the texture pack.", file); + + Chat_Add4("&c Its size is (%i,%i), your GPU supports (%i,%i) at most.", + &bmp->width, &bmp->height, &maxWidth, &maxHeight); + return false; + } + + if (Gfx.MaxTexSize && (bmp->width * bmp->height > Gfx.MaxTexSize)) { + Chat_Add1("&cUnable to use %s from the texture pack.", file); + texSize = (bmp->width * bmp->height) / (1024.0f * 1024.0f); + maxSize = Gfx.MaxTexSize / (1024.0f * 1024.0f); + + Chat_Add2("&c Its size is %f3 MB, your GPU supports %f3 MB at most.", + &texSize, &maxSize); + return false; + } + + return Game_ValidateBitmapPow2(file, bmp); +} + +cc_bool Game_ValidateBitmapPow2(const cc_string* file, struct Bitmap* bmp) { + if (!Math_IsPowOf2(bmp->width) || !Math_IsPowOf2(bmp->height)) { + Chat_Add1("&cUnable to use %s from the texture pack.", file); + + Chat_Add2("&c Its size is (%i,%i), which is not a power of two size.", + &bmp->width, &bmp->height); + return false; + } + return true; +} + +void Game_UpdateDimensions(void) { + Game.Width = max(Window_Main.Width, 1); + Game.Height = max(Window_Main.Height, 1); +} + +static void Game_OnResize(void* obj) { + Game_UpdateDimensions(); + Gfx_OnWindowResize(); + Camera_UpdateProjection(); +} + +static void HandleOnNewMap(void* obj) { + struct IGameComponent* comp; + for (comp = comps_head; comp; comp = comp->next) { + if (comp->OnNewMap) comp->OnNewMap(); + } +} + +static void HandleOnNewMapLoaded(void* obj) { + struct IGameComponent* comp; + for (comp = comps_head; comp; comp = comp->next) { + if (comp->OnNewMapLoaded) comp->OnNewMapLoaded(); + } +} + +static void HandleInactiveChanged(void* obj) { + if (Window_Main.Inactive) { + Chat_AddOf(&Gfx_LowPerfMessage, MSG_TYPE_EXTRASTATUS_2); + Gfx_SetFpsLimit(false, 1000 / 1.0f); + Gfx.ReducedPerfMode = true; + } else { + Chat_AddOf(&String_Empty, MSG_TYPE_EXTRASTATUS_2); + Game_SetFpsLimit(Game_FpsLimit); + + Gfx.ReducedPerfMode = false; + Gfx.ReducedPerfModeCooldown = 2; + } + +#ifdef CC_BUILD_WEB + extern void emscripten_resume_main_loop(void); + emscripten_resume_main_loop(); +#endif +} + +static void Game_WarnFunc(const cc_string* msg) { + cc_string str = *msg, line; + while (str.length) { + String_UNSAFE_SplitBy(&str, '\n', &line); + Chat_Add1("&c%s", &line); + } +} + +static void LoadOptions(void) { + Game_ClassicMode = Options_GetBool(OPT_CLASSIC_MODE, false); + Game_ClassicHacks = Options_GetBool(OPT_CLASSIC_HACKS, false); + Game_Anaglyph3D = Options_GetBool(OPT_ANAGLYPH3D, false); + Game_ViewBobbing = Options_GetBool(OPT_VIEW_BOBBING, true); + + Game_AllowCustomBlocks = !Game_ClassicMode && Options_GetBool(OPT_CUSTOM_BLOCKS, true); + Game_SimpleArmsAnim = !Game_ClassicMode && Options_GetBool(OPT_SIMPLE_ARMS_ANIM, false); + Game_BreakableLiquids = !Game_ClassicMode && Options_GetBool(OPT_MODIFIABLE_LIQUIDS, false); + Game_AllowServerTextures = !Game_ClassicMode && Options_GetBool(OPT_SERVER_TEXTURES, true); + + Game_ViewDistance = Options_GetInt(OPT_VIEW_DISTANCE, 8, 4096, DEFAULT_VIEWDIST); + Game_UserViewDistance = Game_ViewDistance; + /* TODO: Do we need to support option to skip SSL */ + /*cc_bool skipSsl = Options_GetBool("skip-ssl-check", false); + if (skipSsl) { + ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; + Options.Set("skip-ssl-check", false); + }*/ +} + +#ifdef CC_BUILD_PLUGINS +static void LoadPlugin(const cc_string* path, void* obj) { + void* lib; + void* verSym; /* EXPORT int Plugin_ApiVersion = GAME_API_VER; */ + void* compSym; /* EXPORT struct IGameComponent Plugin_Component = { (whatever) } */ + int ver; + + /* ignore accepted.txt, deskop.ini, .pdb files, etc */ + if (!String_CaselessEnds(path, &DynamicLib_Ext)) return; + /* don't try to load 32 bit plugins on 64 bit OS or vice versa */ + if (sizeof(void*) == 4 && String_ContainsConst(path, "_64.")) return; + if (sizeof(void*) == 8 && String_ContainsConst(path, "_32.")) return; + + lib = DynamicLib_Load2(path); + if (!lib) { Logger_DynamicLibWarn("loading", path); return; } + + verSym = DynamicLib_Get2(lib, "Plugin_ApiVersion"); + if (!verSym) { Logger_DynamicLibWarn("getting version of", path); return; } + compSym = DynamicLib_Get2(lib, "Plugin_Component"); + if (!compSym) { Logger_DynamicLibWarn("initing", path); return; } + + ver = *((int*)verSym); + if (ver < GAME_API_VER) { + Chat_Add1("&c%s plugin is outdated! Try getting a more recent version.", path); + return; + } else if (ver > GAME_API_VER) { + Chat_Add1("&cYour game is too outdated to use %s plugin! Try updating it.", path); + return; + } + + Game_AddComponent((struct IGameComponent*)compSym); +} + +static void LoadPlugins(void) { + static const cc_string dir = String_FromConst("plugins"); + cc_result res; + + Utils_EnsureDirectory("plugins"); + res = Directory_Enum(&dir, NULL, LoadPlugin); + if (res) Logger_SysWarn(res, "enumerating plugins directory"); +} +#else +static void LoadPlugins(void) { } +#endif + +static void Game_Free(void* obj); +static void Game_Load(void) { + struct IGameComponent* comp; + Game_UpdateDimensions(); + Game_SetFpsLimit(Options_GetEnum(OPT_FPS_LIMIT, 0, FpsLimit_Names, FPS_LIMIT_COUNT)); + Gfx_Create(); + + Logger_WarnFunc = Game_WarnFunc; + LoadOptions(); + GameVersion_Load(); + Utils_EnsureDirectory("maps"); + + Event_Register_(&WorldEvents.NewMap, NULL, HandleOnNewMap); + Event_Register_(&WorldEvents.MapLoaded, NULL, HandleOnNewMapLoaded); + Event_Register_(&WindowEvents.Resized, NULL, Game_OnResize); + Event_Register_(&WindowEvents.Closing, NULL, Game_Free); + Event_Register_(&WindowEvents.InactiveChanged, NULL, HandleInactiveChanged); + + Game_AddComponent(&World_Component); + Game_AddComponent(&Textures_Component); + Game_AddComponent(&Input_Component); + Game_AddComponent(&Camera_Component); + Game_AddComponent(&Gfx_Component); + Game_AddComponent(&Blocks_Component); + Game_AddComponent(&Drawer2D_Component); + Game_AddComponent(&SystemFonts_Component); + + Game_AddComponent(&Chat_Component); + Game_AddComponent(&Commands_Component); + Game_AddComponent(&Particles_Component); + Game_AddComponent(&TabList_Component); + Game_AddComponent(&Models_Component); + Game_AddComponent(&Entities_Component); + Game_AddComponent(&Http_Component); + Game_AddComponent(&Lighting_Component); + + Game_AddComponent(&Animations_Component); + Game_AddComponent(&Inventory_Component); + Game_AddComponent(&Builder_Component); + Game_AddComponent(&MapRenderer_Component); + Game_AddComponent(&EnvRenderer_Component); + Game_AddComponent(&Server_Component); + Game_AddComponent(&Protocol_Component); + + Game_AddComponent(&Gui_Component); + Game_AddComponent(&Selections_Component); + Game_AddComponent(&HeldBlockRenderer_Component); + /* Gfx_SetDepthWrite(true) */ + Game_AddComponent(&SelOutlineRenderer_Component); + Game_AddComponent(&Audio_Component); + Game_AddComponent(&AxisLinesRenderer_Component); + Game_AddComponent(&Formats_Component); + Game_AddComponent(&EntityRenderers_Component); + + LoadPlugins(); + for (comp = comps_head; comp; comp = comp->next) { + if (comp->Init) comp->Init(); + } + + TexturePack_ExtractCurrent(true); + if (TexturePack_DefaultMissing) { + Window_ShowDialog("Missing file", + "Both default.zip and classicube.zip are missing,\n try downloading resources first.\n\nClassiCube will still run, but without any textures."); + } + + entTaskI = ScheduledTask_Add(GAME_DEF_TICKS, Entities_Tick); + if (Gfx_WarnIfNecessary()) EnvRenderer_SetMode(EnvRenderer_Minimal | ENV_LEGACY); + Server.BeginConnect(); +} + +void Game_SetFpsLimit(int method) { + float minFrameTime = 0; + Game_FpsLimit = method; + + switch (method) { + case FPS_LIMIT_144: minFrameTime = 1000/144.0f; break; + case FPS_LIMIT_120: minFrameTime = 1000/120.0f; break; + case FPS_LIMIT_60: minFrameTime = 1000/60.0f; break; + case FPS_LIMIT_30: minFrameTime = 1000/30.0f; break; + } + Gfx_SetFpsLimit(method == FPS_LIMIT_VSYNC, minFrameTime); +} + +static void UpdateViewMatrix(void) { + Camera.Active->GetView(&Gfx.View); + FrustumCulling_CalcFrustumEquations(&Gfx.Projection, &Gfx.View); +} + +static void Render3DFrame(float delta, float t) { + Vec3 pos; + Gfx_LoadMatrix(MATRIX_PROJECTION, &Gfx.Projection); + Gfx_LoadMatrix(MATRIX_VIEW, &Gfx.View); + if (EnvRenderer_ShouldRenderSkybox()) EnvRenderer_RenderSkybox(); + + AxisLinesRenderer_Render(); + Entities_RenderModels(delta, t); + EntityNames_Render(); + + Particles_Render(t); + EnvRenderer_RenderSky(); + EnvRenderer_RenderClouds(); + + MapRenderer_Update(delta); + MapRenderer_RenderNormal(delta); + EnvRenderer_RenderMapSides(); + + EntityShadows_Render(); + if (Game_SelectedPos.valid && !Game_HideGui) { + SelOutlineRenderer_Render(&Game_SelectedPos, true); + } + + /* Render water over translucent blocks when under the water outside the map for proper alpha blending */ + pos = Camera.CurrentPos; + if (pos.y < Env.EdgeHeight && (pos.x < 0 || pos.z < 0 || pos.x > World.Width || pos.z > World.Length)) { + MapRenderer_RenderTranslucent(delta); + EnvRenderer_RenderMapEdges(); + } else { + EnvRenderer_RenderMapEdges(); + MapRenderer_RenderTranslucent(delta); + } + + /* Need to render again over top of translucent block, as the selection outline */ + /* is drawn without writing to the depth buffer */ + if (Game_SelectedPos.valid && !Game_HideGui && Blocks.Draw[Game_SelectedPos.block] == DRAW_TRANSLUCENT) { + SelOutlineRenderer_Render(&Game_SelectedPos, false); + } + + Selections_Render(); + EntityNames_RenderHovered(); + if (!Game_HideGui) HeldBlockRenderer_Render(delta); +} + +static void Render3D_Anaglyph(float delta, float t) { + struct Matrix proj = Gfx.Projection; + struct Matrix view = Gfx.View; + + Gfx_Set3DLeft(&proj, &view); + Render3DFrame(delta, t); + + Gfx_Set3DRight(&proj, &view); + Render3DFrame(delta, t); + + Gfx_End3D(&proj, &view); +} + +static void PerformScheduledTasks(double time) { + struct ScheduledTask* task; + int i; + + for (i = 0; i < tasksCount; i++) { + task = &tasks[i]; + task->accumulator += time; + + while (task->accumulator >= task->interval) { + task->Callback(task); + task->accumulator -= task->interval; + } + } +} + +void Game_TakeScreenshot(void) { + cc_string filename; char fileBuffer[STRING_SIZE]; + cc_string path; char pathBuffer[FILENAME_SIZE]; + struct DateTime now; + cc_result res; +#ifdef CC_BUILD_WEB + char str[NATIVE_STR_LEN]; +#else + struct Stream stream; +#endif + Game_ScreenshotRequested = false; + DateTime_CurrentLocal(&now); + + String_InitArray(filename, fileBuffer); + String_Format3(&filename, "screenshot_%p4-%p2-%p2", &now.year, &now.month, &now.day); + String_Format3(&filename, "-%p2-%p2-%p2.png", &now.hour, &now.minute, &now.second); + +#ifdef CC_BUILD_WEB + extern void interop_TakeScreenshot(const char* path); + String_EncodeUtf8(str, &filename); + interop_TakeScreenshot(str); +#else + if (!Utils_EnsureDirectory("screenshots")) return; + String_InitArray(path, pathBuffer); + String_Format1(&path, "screenshots/%s", &filename); + + res = Stream_CreateFile(&stream, &path); + if (res) { Logger_SysWarn2(res, "creating", &path); return; } + + res = Gfx_TakeScreenshot(&stream); + if (res) { + Logger_SysWarn2(res, "saving to", &path); stream.Close(&stream); return; + } + + res = stream.Close(&stream); + if (res) { Logger_SysWarn2(res, "closing", &path); return; } + Chat_Add1("&eTaken screenshot as: %s", &filename); + +#ifdef CC_BUILD_MOBILE + Platform_ShareScreenshot(&filename); +#endif +#endif +} + +static CC_INLINE void Game_DrawFrame(float delta, float t) { + UpdateViewMatrix(); + + if (!Gui_GetBlocksWorld()) { + Camera.Active->GetPickedBlock(&Game_SelectedPos); /* TODO: only pick when necessary */ + Camera_KeyLookUpdate(delta); + InputHandler_Tick(); + + if (Game_Anaglyph3D) { + Render3D_Anaglyph(delta, t); + } else { + Render3DFrame(delta, t); + } + } else { + RayTracer_SetInvalid(&Game_SelectedPos); + } + + Gfx_Begin2D(Game.Width, Game.Height); + Gui_RenderGui(delta); + OnscreenKeyboard_Draw3D(); +/* TODO find a better solution than this */ +#ifdef CC_BUILD_3DS + if (Game_Anaglyph3D) { + extern void Gfx_SetTopRight(void); + Gfx_SetTopRight(); + Gui_RenderGui(delta); + } +#endif + Gfx_End2D(); +} + +#ifdef CC_BUILD_SPLITSCREEN +static void DrawSplitscreen(float delta, float t, int i, int x, int y, int w, int h) { + Gfx_SetViewport(x, y, w, h); + + Entities.CurPlayer = &LocalPlayer_Instances[i]; + LocalPlayer_SetInterpPosition(Entities.CurPlayer, t); + Camera.CurrentPos = Camera.Active->GetPosition(t); + + Game_DrawFrame(delta, t); +} +#endif + +static CC_INLINE void Game_RenderFrame(double delta) { + struct ScheduledTask entTask; + float t; + + /* TODO: Should other tasks get called back too? */ + /* Might not be such a good idea for the http_clearcache, */ + /* don't really want all skins getting lost */ + if (Gfx.LostContext) { + if (Gfx_TryRestoreContext()) { + Gfx_RecreateContext(); + /* all good, context is back */ + } else { + Game.Time += delta; /* TODO: Not set in two places? */ + Server.Tick(NULL); + Thread_Sleep(16); + return; + } + } + + Gfx_BeginFrame(); + Gfx_BindIb(Gfx_defaultIb); + Game.Time += delta; + Game_Vertices = 0; + + if (Input.Sources & INPUT_SOURCE_GAMEPAD) Gamepad_Tick(delta); + Camera.Active->UpdateMouse(Entities.CurPlayer, delta); + + if (!Window_Main.Focused && !Gui.InputGrab) Gui_ShowPauseMenu(); + + if (InputBind_IsPressed(BIND_ZOOM_SCROLL) && !Gui.InputGrab) { + InputHandler_SetFOV(Camera.ZoomFov); + } + + PerformScheduledTasks(delta); + entTask = tasks[entTaskI]; + t = (float)(entTask.accumulator / entTask.interval); + LocalPlayer_SetInterpPosition(Entities.CurPlayer, t); + + Camera.CurrentPos = Camera.Active->GetPosition(t); + /* NOTE: EnvRenderer_UpdateFog also also sets clear color */ + EnvRenderer_UpdateFog(); + AudioBackend_Tick(); + + /* TODO: Not calling Gfx_EndFrame doesn't work with Direct3D9 */ + if (Window_Main.Inactive) return; + Gfx_ClearBuffers(GFX_BUFFER_COLOR | GFX_BUFFER_DEPTH); + +#ifdef CC_BUILD_SPLITSCREEN + switch (Game_NumLocalPlayers) { + case 1: + Game_DrawFrame(delta, t); break; + case 2: + DrawSplitscreen(delta, t, 0, 0, 0, Game.Width, Game.Height / 2); + DrawSplitscreen(delta, t, 1, 0, Game.Height / 2, Game.Width, Game.Height / 2); + break; + case 3: + DrawSplitscreen(delta, t, 0, 0, 0, Game.Width , Game.Height / 2); + DrawSplitscreen(delta, t, 1, 0, Game.Height / 2, Game.Width / 2, Game.Height / 2); + DrawSplitscreen(delta, t, 2, Game.Width / 2, Game.Height / 2, Game.Width / 2, Game.Height / 2); + break; + case 4: + DrawSplitscreen(delta, t, 0, 0, 0, Game.Width / 2, Game.Height / 2); + DrawSplitscreen(delta, t, 1, Game.Width / 2, 0, Game.Width / 2, Game.Height / 2); + DrawSplitscreen(delta, t, 2, 0, Game.Height / 2, Game.Width / 2, Game.Height / 2); + DrawSplitscreen(delta, t, 3, Game.Width / 2, Game.Height / 2, Game.Width / 2, Game.Height / 2); + break; + } +#else + Game_DrawFrame(delta, t); +#endif + + if (Game_ScreenshotRequested) Game_TakeScreenshot(); + Gfx_EndFrame(); +} + +static void Game_Free(void* obj) { + struct IGameComponent* comp; + /* Most components will call OnContextLost in their Free functions */ + /* Set to false so components will always free managed textures too */ + Gfx.ManagedTextures = false; + Event_UnregisterAll(); + tasksCount = 0; + + for (comp = comps_head; comp; comp = comp->next) { + if (comp->Free) comp->Free(); + } + + gameRunning = false; + Logger_WarnFunc = Logger_DialogWarn; + Gfx_Free(); + Options_SaveIfChanged(); + Window_DisableRawMouse(); +} + +#define Game_DoFrameBody() \ + render = Stopwatch_Measure();\ + delta = Stopwatch_ElapsedMicroseconds(Game_FrameStart, render) / (1000.0 * 1000.0);\ + \ + Window_ProcessEvents(delta);\ + if (!gameRunning) return;\ + \ + if (delta > 5.0) delta = 5.0; /* avoid large delta with suspended process */ \ + if (delta > 0.0) { Game_FrameStart = render; Game_RenderFrame(delta); } + +#ifdef CC_BUILD_WEB +void Game_DoFrame(void) { + cc_uint64 render; + double delta; + Game_DoFrameBody() +} + +static void Game_RunLoop(void) { + Game_FrameStart = Stopwatch_Measure(); + /* Window_Web.c sets Game_DoFrame as the main loop callback function */ + /* (i.e. web browser is in charge of calling Game_DoFrame, not us) */ +} + +cc_bool Game_ShouldClose(void) { + if (Server.IsSinglePlayer) { + /* Close if map was saved within last 5 seconds */ + return World.LastSave + 5 >= Game.Time; + } + + /* Try to intercept Ctrl+W or Cmd+W for multiplayer */ + if (Input_IsCtrlPressed() || Input_IsWinPressed()) return false; + /* Also try to intercept mouse back button (Mouse4) */ + return !Input.Pressed[CCMOUSE_X1]; +} +#else +static void Game_RunLoop(void) { + cc_uint64 render; + double delta; + + Game_FrameStart = Stopwatch_Measure(); + for (;;) { Game_DoFrameBody() } +} +#endif + +void Game_Run(int width, int height, const cc_string* title) { + Window_Create3D(width, height); + Window_SetTitle(title); + Window_Show(); + gameRunning = true; + + Game_Load(); + Event_RaiseVoid(&WindowEvents.Resized); + Game_RunLoop(); +} diff --git a/src/Game.h b/src/Game.h new file mode 100644 index 0000000..0cc145f --- /dev/null +++ b/src/Game.h @@ -0,0 +1,157 @@ +#ifndef CC_GAME_H +#define CC_GAME_H +#include "Core.h" +/* Represents the game and related structures. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct Bitmap; +struct Stream; +CC_VAR extern struct _GameData { + /* Width and height of the window. (1 at minimum) */ + int Width, Height; + /* Total time (in seconds) the game has been running for. */ + double Time; + /* Number of chunks updated within last second. Resets to 0 after every second. */ + int ChunkUpdates; +} Game; + +/* Stopwatch measurement of when current frame started */ +extern cc_uint64 Game_FrameStart; +extern struct RayTracer Game_SelectedPos; +extern cc_bool Game_UseCPEBlocks; + +extern cc_string Game_Username; +extern cc_string Game_Mppass; + +#ifdef CC_BUILD_SPLITSCREEN +extern int Game_NumLocalPlayers; +#else +#define Game_NumLocalPlayers 1 +#endif + +#if defined CC_BUILD_N64 + #define DEFAULT_VIEWDIST 20 +#elif defined CC_BUILD_NDS || defined CC_BUILD_PS1 + #define DEFAULT_VIEWDIST 192 +#else + #define DEFAULT_VIEWDIST 512 +#endif +#define DEFAULT_MAX_VIEWDIST 32768 + +extern int Game_ViewDistance; +extern int Game_MaxViewDistance; +extern int Game_UserViewDistance; + +/* Strategy used to limit FPS (see FpsLimitMethod enum) */ +extern int Game_FpsLimit; +extern cc_bool Game_SimpleArmsAnim; +extern int Game_Vertices; + +extern cc_bool Game_ClassicMode; +extern cc_bool Game_ClassicHacks; +#define Game_PureClassic (Game_ClassicMode && !Game_ClassicHacks) +extern cc_bool Game_AllowCustomBlocks; +extern cc_bool Game_AllowServerTextures; + +extern cc_bool Game_Anaglyph3D; +extern cc_bool Game_ViewBobbing; +extern cc_bool Game_BreakableLiquids; +/* Whether a screenshot should be taken at the end of this frame */ +extern cc_bool Game_ScreenshotRequested; +extern cc_bool Game_HideGui; + +enum GAME_VERSION_ { + VERSION_0017 = 27, VERSION_0019 = 28, VERSION_0023 = 29, VERSION_0030 = 30, VERSION_CPE = 31 +}; +struct GameVersion { + const char* Name; + cc_bool HasCPE; + cc_uint8 Version, Protocol, MaxCoreBlock; + cc_uint8 BlocksPerRow, InventorySize; + const cc_uint8* Inventory; + const cc_uint8* Hotbar; + const char* DefaultTexpack; +}; +extern struct GameVersion Game_Version; +extern void GameVersion_Load(void); + +enum FpsLimitMethod { + FPS_LIMIT_VSYNC, FPS_LIMIT_30, FPS_LIMIT_60, FPS_LIMIT_120, FPS_LIMIT_144, FPS_LIMIT_NONE, FPS_LIMIT_COUNT +}; +extern const char* const FpsLimit_Names[FPS_LIMIT_COUNT]; + +void Game_ToggleFullscreen(void); +void Game_CycleViewDistance(void); +/* Attempts to reduce VRAM usage (e.g. reducing view distance) */ +/* Returns false if VRAM cannot be reduced any further */ +cc_bool Game_ReduceVRAM(void); + +void Game_SetViewDistance(int distance); +void Game_UserSetViewDistance(int distance); +void Game_Disconnect(const cc_string* title, const cc_string* reason); +void Game_Reset(void); + +/* Sets the block in the map at the given coordinates, then updates state associated with the block. */ +/* (updating state means recalculating light, redrawing chunk block is in, etc) */ +/* NOTE: This does NOT notify the server, use Game_ChangeBlock for that. */ +CC_API void Game_UpdateBlock(int x, int y, int z, BlockID block); +/* Calls Game_UpdateBlock, then informs server connection of the block change. */ +/* In multiplayer this is sent to the server, in singleplayer just activates physics. */ +CC_API void Game_ChangeBlock(int x, int y, int z, BlockID block); + +cc_bool Game_CanPick(BlockID block); +/* Updates Game_Width and Game_Height. */ +void Game_UpdateDimensions(void); +/* Sets the strategy/method used to limit frames per second. */ +/* See FPS_LIMIT_ for valid strategies/methods */ +void Game_SetFpsLimit(int method); + +cc_bool Game_UpdateTexture(GfxResourceID* texId, struct Stream* src, const cc_string* file, + cc_uint8* skinType, int* heightDivisor); +/* Checks that the given bitmap can be loaded into a native gfx texture. */ +/* (must be power of two size and be <= Gfx_MaxTexWidth/Gfx_MaxHeight) */ +cc_bool Game_ValidateBitmap(const cc_string* file, struct Bitmap* bmp); +/* Checks that the given bitmap is a power of two size */ +/* NOTE: Game_ValidateBitmap should nearly always be used instead of this */ +cc_bool Game_ValidateBitmapPow2(const cc_string* file, struct Bitmap* bmp); + +/* Runs the main game loop until the window is closed. */ +void Game_Run(int width, int height, const cc_string* title); +/* Whether the game should be allowed to automatically close */ +cc_bool Game_ShouldClose(void); + +/* Represents a game component. */ +struct IGameComponent; +struct IGameComponent { + /* Called to init the component's state. (called when game is starting) */ + void (*Init)(void); + /* Called to free the component's state. (called when game is closing) */ + void (*Free)(void); + /* Called to reset the component's state. (e.g. reconnecting to server) */ + void (*Reset)(void); + /* Called to update the component's state when the user begins loading a new map. */ + void (*OnNewMap)(void); + /* Called to update the component's state when the user has finished loading a new map. */ + void (*OnNewMapLoaded)(void); + /* Next component in linked list of components. */ + struct IGameComponent* next; +}; +/* Adds a component to linked list of components. (always at end) */ +CC_NOINLINE void Game_AddComponent(struct IGameComponent* comp); + +/* Represents a task that periodically runs on the main thread every specified interval. */ +struct ScheduledTask; +struct ScheduledTask { + /* How long (in seconds) has elapsed since callback was last invoked */ + double accumulator; + /* How long (in seconds) between invocations of the callback */ + double interval; + /* Callback function that is periodically invoked */ + void (*Callback)(struct ScheduledTask* task); +}; + +typedef void (*ScheduledTaskCallback)(struct ScheduledTask* task); +/* Adds a task to list of scheduled tasks. (always at end) */ +CC_API int ScheduledTask_Add(double interval, ScheduledTaskCallback callback); +#endif diff --git a/src/GameVersion.c b/src/GameVersion.c new file mode 100644 index 0000000..625f4bf --- /dev/null +++ b/src/GameVersion.c @@ -0,0 +1,91 @@ +#include "Game.h" +#include "Protocol.h" +#include "Block.h" +#include "Options.h" +#include "Inventory.h" + +static const cc_uint8 v7_inventory[] = { + BLOCK_STONE, BLOCK_COBBLE, BLOCK_BRICK, BLOCK_DIRT, BLOCK_WOOD, BLOCK_LOG, BLOCK_LEAVES, BLOCK_GLASS, BLOCK_SLAB, + BLOCK_MOSSY_ROCKS, BLOCK_SAPLING, BLOCK_DANDELION, BLOCK_ROSE, BLOCK_BROWN_SHROOM, BLOCK_RED_SHROOM, BLOCK_SAND, BLOCK_GRAVEL, BLOCK_SPONGE, + BLOCK_RED, BLOCK_ORANGE, BLOCK_YELLOW, BLOCK_LIME, BLOCK_GREEN, BLOCK_TEAL, BLOCK_AQUA, BLOCK_CYAN, BLOCK_BLUE, + BLOCK_INDIGO, BLOCK_VIOLET, BLOCK_MAGENTA, BLOCK_PINK, BLOCK_BLACK, BLOCK_GRAY, BLOCK_WHITE, BLOCK_COAL_ORE, BLOCK_IRON_ORE, + BLOCK_GOLD_ORE, BLOCK_IRON, BLOCK_GOLD, BLOCK_BOOKSHELF, BLOCK_TNT, BLOCK_OBSIDIAN, +}; +static const cc_uint8 v6_inventory[] = { + BLOCK_STONE, BLOCK_COBBLE, BLOCK_DIRT, BLOCK_WOOD, BLOCK_LOG, BLOCK_LEAVES, BLOCK_SAPLING, BLOCK_DANDELION, + BLOCK_ROSE, BLOCK_BROWN_SHROOM, BLOCK_RED_SHROOM, BLOCK_SAND, BLOCK_GRAVEL, BLOCK_GLASS, BLOCK_SPONGE, BLOCK_GOLD, + BLOCK_RED, BLOCK_ORANGE, BLOCK_YELLOW, BLOCK_LIME, BLOCK_GREEN, BLOCK_TEAL, BLOCK_AQUA, BLOCK_CYAN, BLOCK_BLUE, + BLOCK_INDIGO, BLOCK_VIOLET, BLOCK_MAGENTA, BLOCK_PINK, BLOCK_BLACK, BLOCK_GRAY, BLOCK_WHITE, +}; +static const cc_uint8 v5_inventory[] = { + BLOCK_STONE, BLOCK_COBBLE, BLOCK_DIRT, BLOCK_WOOD, BLOCK_LOG, BLOCK_LEAVES, + BLOCK_SAPLING, BLOCK_SAND, BLOCK_GRAVEL, BLOCK_GLASS, BLOCK_SPONGE, +}; +static const cc_uint8 v4_inventory[] = { + BLOCK_STONE, BLOCK_COBBLE, BLOCK_DIRT, BLOCK_WOOD, BLOCK_LOG, BLOCK_LEAVES, + BLOCK_SAPLING, BLOCK_SAND, BLOCK_GRAVEL, +}; + +static const cc_uint8 v7_hotbar[INVENTORY_BLOCKS_PER_HOTBAR] = { + BLOCK_STONE, BLOCK_COBBLE, BLOCK_BRICK, BLOCK_DIRT, BLOCK_WOOD, BLOCK_LOG, BLOCK_LEAVES, BLOCK_GLASS, BLOCK_SLAB +}; +static const cc_uint8 v6_hotbar[INVENTORY_BLOCKS_PER_HOTBAR] = { + BLOCK_STONE, BLOCK_COBBLE, BLOCK_DIRT, BLOCK_WOOD, BLOCK_LOG, BLOCK_LEAVES, BLOCK_SAPLING, BLOCK_DANDELION, BLOCK_ROSE +}; +static const cc_uint8 v5_hotbar[INVENTORY_BLOCKS_PER_HOTBAR] = { + BLOCK_STONE, BLOCK_DIRT, BLOCK_SPONGE, BLOCK_WOOD, BLOCK_SAPLING, BLOCK_LOG, BLOCK_LEAVES, BLOCK_GLASS, BLOCK_GRAVEL +}; +static const cc_uint8 v4_hotbar[INVENTORY_BLOCKS_PER_HOTBAR] = { + BLOCK_STONE, BLOCK_DIRT, BLOCK_COBBLE, BLOCK_WOOD, BLOCK_SAPLING, BLOCK_LOG, BLOCK_LEAVES, BLOCK_SAND, BLOCK_GRAVEL +}; + +static const struct GameVersion version_cpe = { + "0.30", true, VERSION_CPE, + PROTOCOL_0030, BLOCK_MAX_CPE, + 10, sizeof(v7_inventory), NULL, v7_hotbar, + "texpacks/default.zip" +}; +static const struct GameVersion version_0030 = { + "0.30", false, VERSION_0030, + PROTOCOL_0030, BLOCK_OBSIDIAN, + 9, sizeof(v7_inventory), v7_inventory, v7_hotbar, + "texpacks/default.zip" +}; +static const struct GameVersion version_0023 = { + "0.0.23a", false, VERSION_0023, + PROTOCOL_0020, BLOCK_GOLD, + 8, sizeof(v6_inventory), v6_inventory, v6_hotbar, + "texpacks/default_0023.zip" +}; +static const struct GameVersion version_0019 = { + "0.0.19a", false, VERSION_0019, + PROTOCOL_0019, BLOCK_GLASS, + 6, sizeof(v5_inventory), v5_inventory, v5_hotbar, + "texpacks/default_0023.zip" +}; +static const struct GameVersion version_0017 = { + "0.0.17a", false, VERSION_0017, + PROTOCOL_0017, BLOCK_LEAVES, + 6, sizeof(v4_inventory), v4_inventory, v4_hotbar, + "texpacks/default_0023.zip" +}; + +void GameVersion_Load(void) { + cc_bool hasCPE = !Game_ClassicMode && Options_GetBool(OPT_CPE, true); + int version = Options_GetInt(OPT_GAME_VERSION, VERSION_0017, VERSION_0030, VERSION_0030); + const struct GameVersion* ver = &version_cpe; + + if (hasCPE) { + /* defaults to CPE already */ + } else if (version == VERSION_0030) { + ver = &version_0030; + } else if (version == VERSION_0023) { + ver = &version_0023; + } else if (version == VERSION_0019) { + ver = &version_0019; + } else if (version == VERSION_0017) { + ver = &version_0017; + } + + Game_Version = *ver; +} diff --git a/src/Generator.c b/src/Generator.c new file mode 100644 index 0000000..ae9ae5c --- /dev/null +++ b/src/Generator.c @@ -0,0 +1,817 @@ +#include "Generator.h" +#include "BlockID.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Platform.h" +#include "World.h" +#include "Utils.h" +#include "Game.h" +#include "Window.h" + +const struct MapGenerator* Gen_Active; +BlockRaw* Gen_Blocks; +int Gen_Seed; + +volatile float Gen_CurrentProgress; +volatile const char* Gen_CurrentState; +volatile cc_bool gen_done; + +/* There are two main types of multitasking: */ +/* - Pre-emptive multitasking (system automatically switches between threads) */ +/* - Cooperative multitasking (threads must be manually switched by the app) */ +/* */ +/* Systems only supporting cooperative multitasking can be problematic though: */ +/* If the whole map generation was performed as a single function call, */ +/* then the game thread would not get run at all until map generation */ +/* completed - which is not a great user experience. */ +/* To avoid that, on these systems, map generation may be divided into */ +/* a series of steps so that ClassiCube can periodically switch back */ +/* to the game thread to ensure that the game itself still (slowly) runs. */ +#ifdef CC_BUILD_COOPTHREADED +static int gen_step; +static cc_uint64 lastRender; + +#define GEN_COOP_BEGIN \ + cc_uint64 curTime; \ + switch (gen_step) { + +#define GEN_COOP_STEP(index, step) \ + case index: \ + step; \ + gen_step++; \ + curTime = Stopwatch_Measure(); \ + if (Stopwatch_ElapsedMS(lastRender, curTime) > 100) { lastRender = curTime; return; } + /* Switch back to game thread if more than 100 milliseconds since it was last run */ + +#define GEN_COOP_END \ + } + +static void Gen_Run(void) { + gen_step = 0; + lastRender = Stopwatch_Measure(); + Gen_Active->Generate(); +} + +cc_bool Gen_IsDone(void) { + /* Resume map generation if incomplete */ + if (!gen_done) Gen_Active->Generate(); + return gen_done; +} +#else +/* For systems supporting preemptive threading, there's no point */ +/* bothering with all the cooperative tasking shenanigans */ +#define GEN_COOP_BEGIN +#define GEN_COOP_STEP(index, step) step; +#define GEN_COOP_END + +static void Gen_DoGen(void) { + Gen_Active->Generate(); +} + +static void Gen_Run(void) { + void* thread; + Thread_Run(&thread, Gen_DoGen, 128 * 1024, "Map gen"); + Thread_Detach(thread); +} + +cc_bool Gen_IsDone(void) { return gen_done; } +#endif + +static void Gen_Reset(void) { + Gen_CurrentProgress = 0.0f; + Gen_CurrentState = ""; + gen_done = false; +} + +void Gen_Start(void) { + Gen_Reset(); + Gen_Blocks = (BlockRaw*)Mem_TryAlloc(World.Volume, 1); + + if (!Gen_Blocks || !Gen_Active->Prepare()) { + Window_ShowDialog("Out of memory", "Not enough free memory to generate a map that large.\nTry a smaller size."); + gen_done = true; + } else { + Gen_Run(); + } +} + + +/*########################################################################################################################* +*-----------------------------------------------------Flatgrass gen-------------------------------------------------------* +*#########################################################################################################################*/ +static void FlatgrassGen_MapSet(int yBeg, int yEnd, BlockRaw block) { + cc_uint32 oneY = (cc_uint32)World.OneY; + BlockRaw* ptr = Gen_Blocks; + int y, yHeight; + + yBeg = max(yBeg, 0); yEnd = max(yEnd, 0); + yHeight = (yEnd - yBeg) + 1; + Gen_CurrentProgress = 0.0f; + + for (y = yBeg; y <= yEnd; y++) { + Mem_Set(ptr + y * oneY, block, oneY); + Gen_CurrentProgress = (float)(y - yBeg) / yHeight; + } +} + +static cc_bool FlatgrassGen_Prepare(void) { + return true; +} + +static void FlatgrassGen_Generate(void) { + Gen_CurrentState = "Setting air blocks"; + FlatgrassGen_MapSet(World.Height / 2, World.MaxY, BLOCK_AIR); + + Gen_CurrentState = "Setting dirt blocks"; + FlatgrassGen_MapSet(0, World.Height / 2 - 2, BLOCK_DIRT); + + Gen_CurrentState = "Setting grass blocks"; + FlatgrassGen_MapSet(World.Height / 2 - 1, World.Height / 2 - 1, BLOCK_GRASS); + + gen_done = true; +} + +const struct MapGenerator FlatgrassGen = { + FlatgrassGen_Prepare, + FlatgrassGen_Generate +}; + + +/*########################################################################################################################* +*---------------------------------------------------Noise generation------------------------------------------------------* +*#########################################################################################################################*/ +#define NOISE_TABLE_SIZE 512 +static void ImprovedNoise_Init(cc_uint8* p, RNGState* rnd) { + cc_uint8 tmp; + int i, j; + for (i = 0; i < 256; i++) { p[i] = i; } + + /* shuffle randomly using fisher-yates */ + for (i = 0; i < 256; i++) { + j = Random_Range(rnd, i, 256); + tmp = p[i]; p[i] = p[j]; p[j] = tmp; + } + + for (i = 0; i < 256; i++) { + p[i + 256] = p[i]; + } +} + +static float ImprovedNoise_Calc(const cc_uint8* p, float x, float y) { + int xFloor, yFloor, X, Y; + float u, v; + int A, B, hash; + float g22, g12, c1; + float g21, g11, c2; + + xFloor = x >= 0 ? (int)x : (int)x - 1; + yFloor = y >= 0 ? (int)y : (int)y - 1; + X = xFloor & 0xFF; Y = yFloor & 0xFF; + x -= xFloor; y -= yFloor; + + u = x * x * x * (x * (x * 6 - 15) + 10); /* Fade(x) */ + v = y * y * y * (y * (y * 6 - 15) + 10); /* Fade(y) */ + A = p[X] + Y; B = p[X + 1] + Y; + + /* Normally, calculating Grad involves a function call. However, we can directly pack this table + (since each value indicates either -1, 0 1) into a set of bit flags. This way we avoid needing + to call another function that performs branching */ +#define xFlags 0x46552222 +#define yFlags 0x2222550A + + hash = (p[p[A]] & 0xF) << 1; + g22 = (((xFlags >> hash) & 3) - 1) * x + (((yFlags >> hash) & 3) - 1) * y; /* Grad(p[p[A], x, y) */ + hash = (p[p[B]] & 0xF) << 1; + g12 = (((xFlags >> hash) & 3) - 1) * (x - 1) + (((yFlags >> hash) & 3) - 1) * y; /* Grad(p[p[B], x - 1, y) */ + c1 = g22 + u * (g12 - g22); + + hash = (p[p[A + 1]] & 0xF) << 1; + g21 = (((xFlags >> hash) & 3) - 1) * x + (((yFlags >> hash) & 3) - 1) * (y - 1); /* Grad(p[p[A + 1], x, y - 1) */ + hash = (p[p[B + 1]] & 0xF) << 1; + g11 = (((xFlags >> hash) & 3) - 1) * (x - 1) + (((yFlags >> hash) & 3) - 1) * (y - 1); /* Grad(p[p[B + 1], x - 1, y - 1) */ + c2 = g21 + u * (g11 - g21); + + return c1 + v * (c2 - c1); +} + + +struct OctaveNoise { cc_uint8 p[8][NOISE_TABLE_SIZE]; int octaves; }; +static void OctaveNoise_Init(struct OctaveNoise* n, RNGState* rnd, int octaves) { + int i; + n->octaves = octaves; + + for (i = 0; i < octaves; i++) { + ImprovedNoise_Init(n->p[i], rnd); + } +} + +static float OctaveNoise_Calc(const struct OctaveNoise* n, float x, float y) { + float amplitude = 1, freq = 1; + float sum = 0; + int i; + + for (i = 0; i < n->octaves; i++) { + sum += ImprovedNoise_Calc(n->p[i], x * freq, y * freq) * amplitude; + amplitude *= 2.0f; + freq *= 0.5f; + } + return sum; +} + + +struct CombinedNoise { struct OctaveNoise noise1, noise2; }; +static void CombinedNoise_Init(struct CombinedNoise* n, RNGState* rnd, int octaves1, int octaves2) { + OctaveNoise_Init(&n->noise1, rnd, octaves1); + OctaveNoise_Init(&n->noise2, rnd, octaves2); +} + +static float CombinedNoise_Calc(const struct CombinedNoise* n, float x, float y) { + float offset = OctaveNoise_Calc(&n->noise2, x, y); + return OctaveNoise_Calc(&n->noise1, x + offset, y); +} + + +/*########################################################################################################################* +*----------------------------------------------------Notchy map gen-------------------------------------------------------* +*#########################################################################################################################*/ +static int waterLevel, minHeight; +static cc_int16* heightmap; +static RNGState rnd; + +static void NotchyGen_FillOblateSpheroid(int x, int y, int z, float radius, BlockRaw block) { + int xBeg = Math_Floor(max(x - radius, 0)); + int xEnd = Math_Floor(min(x + radius, World.MaxX)); + int yBeg = Math_Floor(max(y - radius, 0)); + int yEnd = Math_Floor(min(y + radius, World.MaxY)); + int zBeg = Math_Floor(max(z - radius, 0)); + int zEnd = Math_Floor(min(z + radius, World.MaxZ)); + + float radiusSq = radius * radius; + int index; + int xx, yy, zz, dx, dy, dz; + + for (yy = yBeg; yy <= yEnd; yy++) { dy = yy - y; + for (zz = zBeg; zz <= zEnd; zz++) { dz = zz - z; + for (xx = xBeg; xx <= xEnd; xx++) { dx = xx - x; + + if ((dx * dx + 2 * dy * dy + dz * dz) < radiusSq) { + index = World_Pack(xx, yy, zz); + if (Gen_Blocks[index] == BLOCK_STONE) + Gen_Blocks[index] = block; + } + } + } + } +} + +#define STACK_FAST 8192 +static void NotchyGen_FloodFill(int index, BlockRaw block) { + int* stack; + int stack_default[STACK_FAST]; /* avoid allocating memory if possible */ + int count = 0, limit = STACK_FAST; + int x, y, z; + + stack = stack_default; + if (index < 0) return; /* y below map, don't bother starting */ + stack[count++] = index; + + while (count) { + index = stack[--count]; + + if (Gen_Blocks[index] != BLOCK_AIR) continue; + Gen_Blocks[index] = block; + + x = index % World.Width; + y = index / World.OneY; + z = (index / World.Width) % World.Length; + + /* need to increase stack */ + if (count >= limit - FACE_COUNT) { + Utils_Resize((void**)&stack, &limit, 4, STACK_FAST, STACK_FAST); + } + + if (x > 0) { stack[count++] = index - 1; } + if (x < World.MaxX) { stack[count++] = index + 1; } + if (z > 0) { stack[count++] = index - World.Width; } + if (z < World.MaxZ) { stack[count++] = index + World.Width; } + if (y > 0) { stack[count++] = index - World.OneY; } + } + if (limit > STACK_FAST) Mem_Free(stack); +} + + +static void NotchyGen_CreateHeightmap(void) { + float hLow, hHigh, height; + int hIndex = 0, adjHeight; + int x, z; + struct CombinedNoise n1, n2; + struct OctaveNoise n3; + + CombinedNoise_Init(&n1, &rnd, 8, 8); + CombinedNoise_Init(&n2, &rnd, 8, 8); + OctaveNoise_Init(&n3, &rnd, 6); + + Gen_CurrentState = "Building heightmap"; + for (z = 0; z < World.Length; z++) { + Gen_CurrentProgress = (float)z / World.Length; + + for (x = 0; x < World.Width; x++) { + hLow = CombinedNoise_Calc(&n1, x * 1.3f, z * 1.3f) / 6 - 4; + height = hLow; + + if (OctaveNoise_Calc(&n3, (float)x, (float)z) <= 0) { + hHigh = CombinedNoise_Calc(&n2, x * 1.3f, z * 1.3f) / 5 + 6; + height = max(hLow, hHigh); + } + + height *= 0.5f; + if (height < 0) height *= 0.8f; + + adjHeight = (int)(height + waterLevel); + minHeight = min(adjHeight, minHeight); + heightmap[hIndex++] = adjHeight; + } + } +} + +static int NotchyGen_CreateStrataFast(void) { + cc_uint32 oneY = (cc_uint32)World.OneY; + int stoneHeight, airHeight; + int y; + + Gen_CurrentProgress = 0.0f; + Gen_CurrentState = "Filling map"; + /* Make lava layer at bottom */ + Mem_Set(Gen_Blocks, BLOCK_STILL_LAVA, oneY); + + /* Invariant: the lowest value dirtThickness can possible be is -14 */ + stoneHeight = minHeight - 14; + /* We can quickly fill in bottom solid layers */ + for (y = 1; y <= stoneHeight; y++) { + Mem_Set(Gen_Blocks + y * oneY, BLOCK_STONE, oneY); + Gen_CurrentProgress = (float)y / World.Height; + } + + /* Fill in rest of map wih air */ + airHeight = max(0, stoneHeight) + 1; + for (y = airHeight; y < World.Height; y++) { + Mem_Set(Gen_Blocks + y * oneY, BLOCK_AIR, oneY); + Gen_CurrentProgress = (float)y / World.Height; + } + + /* if stoneHeight is <= 0, then no layer is fully stone */ + return max(stoneHeight, 1); +} + +static void NotchyGen_CreateStrata(void) { + int dirtThickness, dirtHeight; + int minStoneY, stoneHeight; + int hIndex = 0, maxY = World.MaxY, index = 0; + int x, y, z; + struct OctaveNoise n; + + /* Try to bulk fill bottom of the map if possible */ + minStoneY = NotchyGen_CreateStrataFast(); + OctaveNoise_Init(&n, &rnd, 8); + + Gen_CurrentState = "Creating strata"; + for (z = 0; z < World.Length; z++) { + Gen_CurrentProgress = (float)z / World.Length; + + for (x = 0; x < World.Width; x++) { + dirtThickness = (int)(OctaveNoise_Calc(&n, (float)x, (float)z) / 24 - 4); + dirtHeight = heightmap[hIndex++]; + stoneHeight = dirtHeight + dirtThickness; + + stoneHeight = min(stoneHeight, maxY); + dirtHeight = min(dirtHeight, maxY); + + index = World_Pack(x, minStoneY, z); + for (y = minStoneY; y <= stoneHeight; y++) { + Gen_Blocks[index] = BLOCK_STONE; index += World.OneY; + } + + stoneHeight = max(stoneHeight, 0); + index = World_Pack(x, (stoneHeight + 1), z); + for (y = stoneHeight + 1; y <= dirtHeight; y++) { + Gen_Blocks[index] = BLOCK_DIRT; index += World.OneY; + } + } + } +} + +static void NotchyGen_CarveCaves(void) { + int cavesCount, caveLen; + float caveX, caveY, caveZ; + float theta, deltaTheta, phi, deltaPhi; + float caveRadius, radius; + int cenX, cenY, cenZ; + int i, j; + + cavesCount = World.Volume / 8192; + Gen_CurrentState = "Carving caves"; + for (i = 0; i < cavesCount; i++) { + Gen_CurrentProgress = (float)i / cavesCount; + + caveX = (float)Random_Next(&rnd, World.Width); + caveY = (float)Random_Next(&rnd, World.Height); + caveZ = (float)Random_Next(&rnd, World.Length); + + caveLen = (int)(Random_Float(&rnd) * Random_Float(&rnd) * 200.0f); + theta = Random_Float(&rnd) * 2.0f * MATH_PI; deltaTheta = 0.0f; + phi = Random_Float(&rnd) * 2.0f * MATH_PI; deltaPhi = 0.0f; + caveRadius = Random_Float(&rnd) * Random_Float(&rnd); + + for (j = 0; j < caveLen; j++) { + caveX += Math_SinF(theta) * Math_CosF(phi); + caveZ += Math_CosF(theta) * Math_CosF(phi); + caveY += Math_SinF(phi); + + theta = theta + deltaTheta * 0.2f; + deltaTheta = deltaTheta * 0.9f + Random_Float(&rnd) - Random_Float(&rnd); + phi = phi * 0.5f + deltaPhi * 0.25f; + deltaPhi = deltaPhi * 0.75f + Random_Float(&rnd) - Random_Float(&rnd); + if (Random_Float(&rnd) < 0.25f) continue; + + cenX = (int)(caveX + (Random_Next(&rnd, 4) - 2) * 0.2f); + cenY = (int)(caveY + (Random_Next(&rnd, 4) - 2) * 0.2f); + cenZ = (int)(caveZ + (Random_Next(&rnd, 4) - 2) * 0.2f); + + radius = (World.Height - cenY) / (float)World.Height; + radius = 1.2f + (radius * 3.5f + 1.0f) * caveRadius; + radius = radius * Math_SinF(j * MATH_PI / caveLen); + NotchyGen_FillOblateSpheroid(cenX, cenY, cenZ, radius, BLOCK_AIR); + } + } +} + +static void NotchyGen_CarveOreVeins(float abundance, const char* state, BlockRaw block) { + int numVeins, veinLen; + float veinX, veinY, veinZ; + float theta, deltaTheta, phi, deltaPhi; + float radius; + int i, j; + + numVeins = (int)(World.Volume * abundance / 16384); + Gen_CurrentState = state; + for (i = 0; i < numVeins; i++) { + Gen_CurrentProgress = (float)i / numVeins; + + veinX = (float)Random_Next(&rnd, World.Width); + veinY = (float)Random_Next(&rnd, World.Height); + veinZ = (float)Random_Next(&rnd, World.Length); + + veinLen = (int)(Random_Float(&rnd) * Random_Float(&rnd) * 75 * abundance); + theta = Random_Float(&rnd) * 2.0f * MATH_PI; deltaTheta = 0.0f; + phi = Random_Float(&rnd) * 2.0f * MATH_PI; deltaPhi = 0.0f; + + for (j = 0; j < veinLen; j++) { + veinX += Math_SinF(theta) * Math_CosF(phi); + veinZ += Math_CosF(theta) * Math_CosF(phi); + veinY += Math_SinF(phi); + + theta = deltaTheta * 0.2f; + deltaTheta = deltaTheta * 0.9f + Random_Float(&rnd) - Random_Float(&rnd); + phi = phi * 0.5f + deltaPhi * 0.25f; + deltaPhi = deltaPhi * 0.9f + Random_Float(&rnd) - Random_Float(&rnd); + + radius = abundance * Math_SinF(j * MATH_PI / veinLen) + 1.0f; + NotchyGen_FillOblateSpheroid((int)veinX, (int)veinY, (int)veinZ, radius, block); + } + } +} + +static void NotchyGen_FloodFillWaterBorders(void) { + int waterY = waterLevel - 1; + int index1, index2; + int x, z; + Gen_CurrentState = "Flooding edge water"; + + index1 = World_Pack(0, waterY, 0); + index2 = World_Pack(0, waterY, World.Length - 1); + for (x = 0; x < World.Width; x++) { + Gen_CurrentProgress = 0.0f + ((float)x / World.Width) * 0.5f; + + NotchyGen_FloodFill(index1, BLOCK_STILL_WATER); + NotchyGen_FloodFill(index2, BLOCK_STILL_WATER); + index1++; index2++; + } + + index1 = World_Pack(0, waterY, 0); + index2 = World_Pack(World.Width - 1, waterY, 0); + for (z = 0; z < World.Length; z++) { + Gen_CurrentProgress = 0.5f + ((float)z / World.Length) * 0.5f; + + NotchyGen_FloodFill(index1, BLOCK_STILL_WATER); + NotchyGen_FloodFill(index2, BLOCK_STILL_WATER); + index1 += World.Width; index2 += World.Width; + } +} + +static void NotchyGen_FloodFillWater(void) { + int numSources; + int i, x, y, z; + + numSources = World.Width * World.Length / 800; + Gen_CurrentState = "Flooding water"; + for (i = 0; i < numSources; i++) { + Gen_CurrentProgress = (float)i / numSources; + + x = Random_Next(&rnd, World.Width); + z = Random_Next(&rnd, World.Length); + y = waterLevel - Random_Range(&rnd, 1, 3); + NotchyGen_FloodFill(World_Pack(x, y, z), BLOCK_STILL_WATER); + } +} + +static void NotchyGen_FloodFillLava(void) { + int numSources; + int i, x, y, z; + + numSources = World.Width * World.Length / 20000; + Gen_CurrentState = "Flooding lava"; + for (i = 0; i < numSources; i++) { + Gen_CurrentProgress = (float)i / numSources; + + x = Random_Next(&rnd, World.Width); + z = Random_Next(&rnd, World.Length); + y = (int)((waterLevel - 3) * Random_Float(&rnd) * Random_Float(&rnd)); + NotchyGen_FloodFill(World_Pack(x, y, z), BLOCK_STILL_LAVA); + } +} + +static void NotchyGen_CreateSurfaceLayer(void) { + int hIndex = 0, index; + BlockRaw above; + int x, y, z; + struct OctaveNoise n1, n2; + + OctaveNoise_Init(&n1, &rnd, 8); + OctaveNoise_Init(&n2, &rnd, 8); + + Gen_CurrentState = "Creating surface"; + for (z = 0; z < World.Length; z++) { + Gen_CurrentProgress = (float)z / World.Length; + + for (x = 0; x < World.Width; x++) { + y = heightmap[hIndex++]; + if (y < 0 || y >= World.Height) continue; + + index = World_Pack(x, y, z); + above = y >= World.MaxY ? BLOCK_AIR : Gen_Blocks[index + World.OneY]; + + /* TODO: update heightmap */ + if (above == BLOCK_STILL_WATER && (OctaveNoise_Calc(&n2, (float)x, (float)z) > 12)) { + Gen_Blocks[index] = BLOCK_GRAVEL; + } else if (above == BLOCK_AIR) { + Gen_Blocks[index] = (y <= waterLevel && (OctaveNoise_Calc(&n1, (float)x, (float)z) > 8)) ? BLOCK_SAND : BLOCK_GRASS; + } + } + } +} + +static void NotchyGen_PlantFlowers(void) { + int numPatches; + BlockRaw block; + int patchX, patchZ; + int flowerX, flowerY, flowerZ; + int i, j, k, index; + + if (Game_Version.Version < VERSION_0023) return; + numPatches = World.Width * World.Length / 3000; + Gen_CurrentState = "Planting flowers"; + + for (i = 0; i < numPatches; i++) { + Gen_CurrentProgress = (float)i / numPatches; + + block = (BlockRaw)(BLOCK_DANDELION + Random_Next(&rnd, 2)); + patchX = Random_Next(&rnd, World.Width); + patchZ = Random_Next(&rnd, World.Length); + + for (j = 0; j < 10; j++) { + flowerX = patchX; flowerZ = patchZ; + for (k = 0; k < 5; k++) { + flowerX += Random_Next(&rnd, 6) - Random_Next(&rnd, 6); + flowerZ += Random_Next(&rnd, 6) - Random_Next(&rnd, 6); + + if (!World_ContainsXZ(flowerX, flowerZ)) continue; + flowerY = heightmap[flowerZ * World.Width + flowerX] + 1; + if (flowerY <= 0 || flowerY >= World.Height) continue; + + index = World_Pack(flowerX, flowerY, flowerZ); + if (Gen_Blocks[index] == BLOCK_AIR && Gen_Blocks[index - World.OneY] == BLOCK_GRASS) + Gen_Blocks[index] = block; + } + } + } +} + +static void NotchyGen_PlantMushrooms(void) { + int numPatches, groundHeight; + BlockRaw block; + int patchX, patchY, patchZ; + int mushX, mushY, mushZ; + int i, j, k, index; + + if (Game_Version.Version < VERSION_0023) return; + numPatches = World.Volume / 2000; + Gen_CurrentState = "Planting mushrooms"; + + for (i = 0; i < numPatches; i++) { + Gen_CurrentProgress = (float)i / numPatches; + + block = (BlockRaw)(BLOCK_BROWN_SHROOM + Random_Next(&rnd, 2)); + patchX = Random_Next(&rnd, World.Width); + patchY = Random_Next(&rnd, World.Height); + patchZ = Random_Next(&rnd, World.Length); + + for (j = 0; j < 20; j++) { + mushX = patchX; mushY = patchY; mushZ = patchZ; + for (k = 0; k < 5; k++) { + mushX += Random_Next(&rnd, 6) - Random_Next(&rnd, 6); + mushZ += Random_Next(&rnd, 6) - Random_Next(&rnd, 6); + + if (!World_ContainsXZ(mushX, mushZ)) continue; + groundHeight = heightmap[mushZ * World.Width + mushX]; + if (mushY >= (groundHeight - 1)) continue; + + index = World_Pack(mushX, mushY, mushZ); + if (Gen_Blocks[index] == BLOCK_AIR && Gen_Blocks[index - World.OneY] == BLOCK_STONE) + Gen_Blocks[index] = block; + } + } + } +} + +static void NotchyGen_PlantTrees(void) { + int numPatches; + int patchX, patchZ; + int treeX, treeY, treeZ; + int treeHeight, index, count; + BlockRaw under; + int i, j, k, m; + + IVec3 coords[TREE_MAX_COUNT]; + BlockRaw blocks[TREE_MAX_COUNT]; + + Tree_Blocks = Gen_Blocks; + Tree_Rnd = &rnd; + + numPatches = World.Width * World.Length / 4000; + Gen_CurrentState = "Planting trees"; + for (i = 0; i < numPatches; i++) { + Gen_CurrentProgress = (float)i / numPatches; + + patchX = Random_Next(&rnd, World.Width); + patchZ = Random_Next(&rnd, World.Length); + + for (j = 0; j < 20; j++) { + treeX = patchX; treeZ = patchZ; + for (k = 0; k < 20; k++) { + treeX += Random_Next(&rnd, 6) - Random_Next(&rnd, 6); + treeZ += Random_Next(&rnd, 6) - Random_Next(&rnd, 6); + + if (!World_ContainsXZ(treeX, treeZ) || Random_Float(&rnd) >= 0.25f) continue; + treeY = heightmap[treeZ * World.Width + treeX] + 1; + if (treeY >= World.Height) continue; + treeHeight = 5 + Random_Next(&rnd, 3); + + index = World_Pack(treeX, treeY, treeZ); + under = treeY > 0 ? Gen_Blocks[index - World.OneY] : BLOCK_AIR; + + if (under == BLOCK_GRASS && TreeGen_CanGrow(treeX, treeY, treeZ, treeHeight)) { + count = TreeGen_Grow(treeX, treeY, treeZ, treeHeight, coords, blocks); + + for (m = 0; m < count; m++) { + index = World_Pack(coords[m].x, coords[m].y, coords[m].z); + Gen_Blocks[index] = blocks[m]; + } + } + } + } + } +} + +static cc_bool NotchyGen_Prepare(void) { + Random_Seed(&rnd, Gen_Seed); + waterLevel = World.Height / 2; + minHeight = World.Height; + + heightmap = (cc_int16*)Mem_TryAlloc(World.Width * World.Length, 2); + return heightmap != NULL; +} + +static void NotchyGen_Generate(void) { + GEN_COOP_BEGIN + GEN_COOP_STEP( 0, NotchyGen_CreateHeightmap() ); + GEN_COOP_STEP( 1, NotchyGen_CreateStrata() ); + GEN_COOP_STEP( 2, NotchyGen_CarveCaves() ); + GEN_COOP_STEP( 3, NotchyGen_CarveOreVeins(0.9f, "Carving coal ore", BLOCK_COAL_ORE) ); + GEN_COOP_STEP( 4, NotchyGen_CarveOreVeins(0.7f, "Carving iron ore", BLOCK_IRON_ORE) ); + GEN_COOP_STEP( 5, NotchyGen_CarveOreVeins(0.5f, "Carving gold ore", BLOCK_GOLD_ORE) ); + + GEN_COOP_STEP( 6, NotchyGen_FloodFillWaterBorders() ); + GEN_COOP_STEP( 7, NotchyGen_FloodFillWater() ); + GEN_COOP_STEP( 8, NotchyGen_FloodFillLava() ); + + GEN_COOP_STEP( 9, NotchyGen_CreateSurfaceLayer() ); + GEN_COOP_STEP(10, NotchyGen_PlantFlowers() ); + GEN_COOP_STEP(11, NotchyGen_PlantMushrooms() ); + GEN_COOP_STEP(12, NotchyGen_PlantTrees() ); + GEN_COOP_END + + Mem_Free(heightmap); + heightmap = NULL; + gen_done = true; +} + +const struct MapGenerator NotchyGen = { + NotchyGen_Prepare, + NotchyGen_Generate +}; + + +/*########################################################################################################################* +*----------------------------------------------------Tree generation------------------------------------------------------* +*#########################################################################################################################*/ +BlockRaw* Tree_Blocks; +RNGState* Tree_Rnd; + +cc_bool TreeGen_CanGrow(int treeX, int treeY, int treeZ, int treeHeight) { + int baseHeight = treeHeight - 4; + int index; + int x, y, z; + + /* check tree base */ + for (y = treeY; y < treeY + baseHeight; y++) { + for (z = treeZ - 1; z <= treeZ + 1; z++) { + for (x = treeX - 1; x <= treeX + 1; x++) { + + if (!World_Contains(x, y, z)) return false; + index = World_Pack(x, y, z); + if (Tree_Blocks[index] != BLOCK_AIR) return false; + } + } + } + + /* and also check canopy */ + for (y = treeY + baseHeight; y < treeY + treeHeight; y++) { + for (z = treeZ - 2; z <= treeZ + 2; z++) { + for (x = treeX - 2; x <= treeX + 2; x++) { + + if (!World_Contains(x, y, z)) return false; + index = World_Pack(x, y, z); + if (Tree_Blocks[index] != BLOCK_AIR) return false; + } + } + } + return true; +} + +#define TreeGen_Place(xVal, yVal, zVal, block)\ +coords[count].x = (xVal); coords[count].y = (yVal); coords[count].z = (zVal);\ +blocks[count] = block; count++; + +int TreeGen_Grow(int treeX, int treeY, int treeZ, int height, IVec3* coords, BlockRaw* blocks) { + int topStart = treeY + (height - 2); + int count = 0; + int xx, zz, x, y, z; + + /* leaves bottom layer */ + for (y = treeY + (height - 4); y < topStart; y++) { + for (zz = -2; zz <= 2; zz++) { + for (xx = -2; xx <= 2; xx++) { + x = treeX + xx; z = treeZ + zz; + + if (Math_AbsI(xx) == 2 && Math_AbsI(zz) == 2) { + if (Random_Float(Tree_Rnd) >= 0.5f) { + TreeGen_Place(x, y, z, BLOCK_LEAVES); + } + } else { + TreeGen_Place(x, y, z, BLOCK_LEAVES); + } + } + } + } + + /* leaves top layer */ + for (; y < treeY + height; y++) { + for (zz = -1; zz <= 1; zz++) { + for (xx = -1; xx <= 1; xx++) { + x = xx + treeX; z = zz + treeZ; + + if (xx == 0 || zz == 0) { + TreeGen_Place(x, y, z, BLOCK_LEAVES); + } else if (y == topStart && Random_Float(Tree_Rnd) >= 0.5f) { + TreeGen_Place(x, y, z, BLOCK_LEAVES); + } + } + } + } + + /* then place trunk */ + for (y = 0; y < height - 1; y++) { + TreeGen_Place(treeX, treeY + y, treeZ, BLOCK_LOG); + } + return count; +} diff --git a/src/Generator.h b/src/Generator.h new file mode 100644 index 0000000..ebbb197 --- /dev/null +++ b/src/Generator.h @@ -0,0 +1,44 @@ +#ifndef CC_MAP_GEN_H +#define CC_MAP_GEN_H +#include "ExtMath.h" +#include "Vectors.h" +/* Implements flatgrass map generator, and original classic vanilla map generation (with perlin noise) + Based on: https://github.com/ClassiCube/ClassiCube/wiki/Minecraft-Classic-map-generation-algorithm + Thanks to Jerralish for originally reverse engineering classic's algorithm, then preparing a high level overview of the algorithm. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +/* Progress between 0 and 1 for the current step */ +extern volatile float Gen_CurrentProgress; +/* Name of the current step being performed */ +extern volatile const char* Gen_CurrentState; +extern int Gen_Seed; +extern BlockRaw* Gen_Blocks; + +/* Starts generating a map using the Gen_Active generator */ +void Gen_Start(void); +/* Checks whether the map generator has completed yet */ +cc_bool Gen_IsDone(void); + + +struct MapGenerator { + cc_bool (*Prepare)(void); + void (*Generate)(void); +}; + +extern const struct MapGenerator* Gen_Active; +extern const struct MapGenerator FlatgrassGen; +extern const struct MapGenerator NotchyGen; + + +extern BlockRaw* Tree_Blocks; +extern RNGState* Tree_Rnd; +/* Appropriate buffer size to hold positions and blocks generated by the tree generator. */ +#define TREE_MAX_COUNT 96 + +/* Whether a tree can actually be generated at the given coordinates. */ +cc_bool TreeGen_CanGrow(int treeX, int treeY, int treeZ, int treeHeight); +/* Generates the blocks (and their positions in the world) that actually make up a tree. */ +/* Returns the number of blocks generated, which will be <= TREE_MAX_COUNT */ +int TreeGen_Grow(int treeX, int treeY, int treeZ, int height, IVec3* coords, BlockRaw* blocks); +#endif diff --git a/src/Graphics.h b/src/Graphics.h new file mode 100644 index 0000000..ff9c204 --- /dev/null +++ b/src/Graphics.h @@ -0,0 +1,309 @@ +#ifndef CC_GFXAPI_H +#define CC_GFXAPI_H +#include "Vectors.h" +#include "PackedCol.h" +/* +Abstracts a 3D graphics rendering API +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Bitmap; +struct Stream; +struct IGameComponent; +extern struct IGameComponent Gfx_Component; + +typedef enum VertexFormat_ { + VERTEX_FORMAT_COLOURED, VERTEX_FORMAT_TEXTURED +} VertexFormat; +typedef enum FogFunc_ { + FOG_LINEAR, FOG_EXP, FOG_EXP2 +} FogFunc; +typedef enum MatrixType_ { + MATRIX_PROJECTION, MATRIX_VIEW +} MatrixType; + +#define SIZEOF_VERTEX_COLOURED 16 +#define SIZEOF_VERTEX_TEXTURED 24 + +#if defined CC_BUILD_PSP +/* 3 floats for position (XYZ), 4 bytes for colour */ +struct VertexColoured { PackedCol Col; float x, y, z; }; +/* 3 floats for position (XYZ), 2 floats for texture coordinates (UV), 4 bytes for colour */ +struct VertexTextured { float U, V; PackedCol Col; float x, y, z; }; +#else +/* 3 floats for position (XYZ), 4 bytes for colour */ +struct VertexColoured { float x, y, z; PackedCol Col; }; +/* 3 floats for position (XYZ), 2 floats for texture coordinates (UV), 4 bytes for colour */ +struct VertexTextured { float x, y, z; PackedCol Col; float U, V; }; +#endif + +void Gfx_Create(void); +void Gfx_Free(void); + +CC_VAR extern struct _GfxData { + /* Maximum dimensions in pixels that a texture can be created up to */ + /* NOTE: usually 1024 to 16384 */ + int MaxTexWidth, MaxTexHeight; + /* Maximum total size in pixels a texture can consist of */ + /* NOTE: Not all graphics backends specify a value for this */ + int MaxTexSize; + /* Whether context graphics has been lost (all creation/render calls fail) */ + cc_bool LostContext; + /* Whether some textures are created with mipmaps */ + cc_bool Mipmaps; + /* Whether managed textures are actually supported */ + /* If not, you must free/create them just like normal textures */ + cc_bool ManagedTextures; + /* Whether graphics context has been created */ + cc_bool Created; + struct Matrix View, Projection; + /* Whether the graphics backend supports non power of two textures */ + cc_bool SupportsNonPowTwoTextures; + /* Maximum total size in pixels a low resolution texture can consist of */ + /* NOTE: Not all graphics backends specify a value for this */ + int MaxLowResTexSize; + /* Minimum dimensions in pixels that a texture must be */ + /* NOTE: Most graphics backends do not use this */ + int MinTexWidth, MinTexHeight; + cc_bool ReducedPerfMode; + cc_uint8 ReducedPerfModeCooldown; +} Gfx; + +extern GfxResourceID Gfx_defaultIb; +extern const cc_string Gfx_LowPerfMessage; + +#define ICOUNT(verticesCount) (((verticesCount) >> 2) * 6) +#define GFX_MAX_INDICES (65536 / 4 * 6) +#define GFX_MAX_VERTICES 65536 + +typedef enum GfxBuffers_ { + GFX_BUFFER_COLOR = 1, + GFX_BUFFER_DEPTH = 2 +} GfxBuffers; + +/* Texture should persist across gfx context loss (if backend supports ManagedTextures) */ +#define TEXTURE_FLAG_MANAGED 0x01 +/* Texture should allow updating via Gfx_UpdateTexture */ +#define TEXTURE_FLAG_DYNAMIC 0x02 +/* Texture is deliberately (and not accidentally) being created with non power of two dimensions */ +#define TEXTURE_FLAG_NONPOW2 0x04 +/* Texture can fallback to 16 bpp when necessary (most backends don't do this) */ +#define TEXTURE_FLAG_LOWRES 0x08 + +void Gfx_RecreateTexture(GfxResourceID* tex, struct Bitmap* bmp, cc_uint8 flags, cc_bool mipmaps); +void* Gfx_RecreateAndLockVb(GfxResourceID* vb, VertexFormat fmt, int count); + +cc_bool Gfx_CheckTextureSize(int width, int height, cc_uint8 flags); +/* Creates a new texture. (and also generates mipmaps if mipmaps) */ +/* See TEXTURE_FLAG values for supported flags */ +/* NOTE: Only set mipmaps to true if Gfx_Mipmaps is also true, because whether textures +use mipmapping may be either a per-texture or global state depending on the backend */ +CC_API GfxResourceID Gfx_CreateTexture(struct Bitmap* bmp, cc_uint8 flags, cc_bool mipmaps); +GfxResourceID Gfx_CreateTexture2(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps); +/* Updates a region of the given texture. (and mipmapped regions if mipmaps) */ +CC_API void Gfx_UpdateTexturePart(GfxResourceID texId, int x, int y, struct Bitmap* part, cc_bool mipmaps); +/* Updates a region of the given texture. (and mipmapped regions if mipmaps) */ +/* NOTE: rowWidth is in pixels (so for normal bitmaps, rowWidth equals width) */ /* OBSOLETE */ +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps); +/* Sets the currently active texture */ +CC_API void Gfx_BindTexture(GfxResourceID texId); +/* Deletes the given texture, then sets it to 0 */ +CC_API void Gfx_DeleteTexture(GfxResourceID* texId); + +/* NOTE: Completely useless now, and does nothing in all graphics backends */ +/* (used to set whether texture colour is used when rendering vertices) */ +CC_API void Gfx_SetTexturing(cc_bool enabled); +/* Turns on mipmapping. (if Gfx_Mipmaps is enabled) */ +/* NOTE: You must have created textures with mipmaps true for this to work */ +CC_API void Gfx_EnableMipmaps(void); +/* Turns off mipmapping. (if Gfx_Mipmaps is enabled) */ +/* NOTE: You must have created textures with mipmaps true for this to work */ +CC_API void Gfx_DisableMipmaps(void); + +/* Returns whether fog blending is enabled */ +CC_API cc_bool Gfx_GetFog(void); +/* Sets whether fog blending is enabled */ +CC_API void Gfx_SetFog(cc_bool enabled); +/* Sets the colour of the blended fog */ +CC_API void Gfx_SetFogCol(PackedCol col); +/* Sets thickness of fog for FOG_EXP/FOG_EXP2 modes */ +CC_API void Gfx_SetFogDensity(float value); +/* Sets extent/end of fog for FOG_LINEAR mode */ +CC_API void Gfx_SetFogEnd(float value); +/* Sets in what way fog is blended */ +CC_API void Gfx_SetFogMode(FogFunc func); + +/* Sets whether backface culling is performed */ +CC_API void Gfx_SetFaceCulling(cc_bool enabled); +/* Sets whether pixels with an alpha of less than 128 are discarded */ +CC_API void Gfx_SetAlphaTest(cc_bool enabled); +/* Sets whether existing and new pixels are blended together */ +CC_API void Gfx_SetAlphaBlending(cc_bool enabled); +/* Sets whether blending between the alpha components of texture and vertex colour is performed */ +CC_API void Gfx_SetAlphaArgBlend(cc_bool enabled); + +/* Clears the given rendering buffer(s) to default. */ +/* buffers can be either GFX_BUFFER_COLOR or GFX_BUFFER_DEPTH, or both */ +CC_API void Gfx_ClearBuffers(GfxBuffers buffers); +/* Sets the colour that the colour buffer is cleared to */ +CC_API void Gfx_ClearColor(PackedCol color); +/* Sets whether pixels may be discard based on z/depth */ +CC_API void Gfx_SetDepthTest(cc_bool enabled); +/* Sets whether z/depth of pixels is actually written to the depth buffer */ +CC_API void Gfx_SetDepthWrite(cc_bool enabled); +/* Sets whether R/G/B/A of pixels are actually written to the colour buffer channels */ +CC_API void Gfx_SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a); +/* Sets whether the game should only write output to depth buffer */ +/* NOTE: Implicitly calls Gfx_SetColorWrite */ +CC_API void Gfx_DepthOnlyRendering(cc_bool depthOnly); + +/* Anaglyph 3D rendering support */ +void Gfx_Set3DLeft( struct Matrix* proj, struct Matrix* view); +void Gfx_Set3DRight(struct Matrix* proj, struct Matrix* view); +void Gfx_End3D( struct Matrix* proj, struct Matrix* view); + +/* Callback function to initialise/fill out the contents of an index buffer */ +typedef void (*Gfx_FillIBFunc)(cc_uint16* indices, int count, void* obj); +/* Creates a new index buffer and fills out its contents */ +CC_API GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj); +/* Sets the currently active index buffer */ +CC_API void Gfx_BindIb(GfxResourceID ib); +/* Deletes the given index buffer, then sets it to 0 */ +CC_API void Gfx_DeleteIb(GfxResourceID* ib); + +/* Creates a new vertex buffer */ +CC_API GfxResourceID Gfx_CreateVb(VertexFormat fmt, int count); +/* Sets the currently active vertex buffer */ +CC_API void Gfx_BindVb(GfxResourceID vb); +/* Deletes the given vertex buffer, then sets it to 0 */ +CC_API void Gfx_DeleteVb(GfxResourceID* vb); +/* Acquires temp memory for changing the contents of a vertex buffer */ +CC_API void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count); +/* Submits the changed contents of a vertex buffer */ +CC_API void Gfx_UnlockVb(GfxResourceID vb); + +/* TODO: How to make LockDynamicVb work with OpenGL 1.1 Builder stupidity. */ +#ifdef CC_BUILD_GL11 +/* Special case of Gfx_Create/LockVb for building chunks in Builder.c */ +GfxResourceID Gfx_CreateVb2(void* vertices, VertexFormat fmt, int count); +#endif +#if CC_GFX_BACKEND == CC_GFX_BACKEND_GL2 +/* Special case Gfx_BindVb for use with Gfx_DrawIndexedTris_T2fC4b */ +void Gfx_BindVb_Textured(GfxResourceID vb); +#else +#define Gfx_BindVb_Textured Gfx_BindVb +#endif + +/* Creates a new dynamic vertex buffer, whose contents can be updated later */ +CC_API GfxResourceID Gfx_CreateDynamicVb(VertexFormat fmt, int maxVertices); +/* Sets the active vertex buffer to the given dynamic vertex buffer */ +CC_API void Gfx_BindDynamicVb(GfxResourceID vb); +/* Deletes the given dynamic vertex buffer, then sets it to 0 */ +CC_API void Gfx_DeleteDynamicVb(GfxResourceID* vb); +/* Acquires temp memory for changing the contents of a dynamic vertex buffer */ +CC_API void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count); +/* Binds then submits the changed contents of a dynamic vertex buffer */ +CC_API void Gfx_UnlockDynamicVb(GfxResourceID vb); + +/* Updates the data of a dynamic vertex buffer */ +CC_API void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount); + +/* Sets the format of the rendered vertices */ +CC_API void Gfx_SetVertexFormat(VertexFormat fmt); +/* Renders vertices from the currently bound vertex buffer as lines */ +CC_API void Gfx_DrawVb_Lines(int verticesCount); +/* Renders vertices from the currently bound vertex and index buffer as triangles */ +/* NOTE: Offsets each index by startVertex */ +CC_API void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex); +/* Renders vertices from the currently bound vertex and index buffer as triangles */ +CC_API void Gfx_DrawVb_IndexedTris(int verticesCount); +/* Special case Gfx_DrawVb_IndexedTris_Range for map renderer */ +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex); + +/* Loads the given matrix over the currently active matrix */ +CC_API void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix); +/* Loads the identity matrix over the currently active matrix */ +CC_API void Gfx_LoadIdentityMatrix(MatrixType type); +CC_API void Gfx_EnableTextureOffset(float x, float y); +CC_API void Gfx_DisableTextureOffset(void); + +/* Calculates an orthographic projection matrix suitable with this backend. (usually for 2D) */ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar); +/* Calculates a perspective projection matrix suitable with this backend. (usually for 3D) */ +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar); +/* NOTE: Projection matrix calculation is here because it can depend the graphics backend */ +/* (e.g. OpenGL uses a Z clip space range of [-1, 1], whereas Direct3D9 uses [0, 1]) */ + +/* Outputs a .png screenshot of the backbuffer */ +cc_result Gfx_TakeScreenshot(struct Stream* output); +/* Warns in chat if the backend has problems with the user's GPU */ +/* Returns whether legacy rendering mode for borders/sky/clouds is needed */ +cc_bool Gfx_WarnIfNecessary(void); +/* Sets up state for rendering a new frame */ +void Gfx_BeginFrame(void); +/* Finishes rendering a frame, and swaps it with the back buffer */ +void Gfx_EndFrame(void); +/* Sets whether to synchronise with monitor refresh to avoid tearing, and maximum frame rate */ +/* NOTE: VSync setting may be unsupported or just ignored */ +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMillis); +/* Gets information about the user's GPU and current backend state */ +/* Backend state may include depth buffer bits, free memory, etc */ +/* NOTE: Each line is separated by \n */ +void Gfx_GetApiInfo(cc_string* info); + +/* Updates state when the window's dimensions have changed */ +/* NOTE: This may require recreating the context depending on the backend */ +void Gfx_OnWindowResize(void); +void Gfx_SetViewport(int x, int y, int w, int h); + +enum Screen3DS { TOP_SCREEN, BOTTOM_SCREEN }; +#ifdef CC_BUILD_DUALSCREEN +/* Selects which screen on the 3DS to render to */ +void Gfx_3DS_SetRenderScreen(enum Screen3DS screen); +#else +static inline +void Gfx_3DS_SetRenderScreen(enum Screen3DS screen) { } +#endif + +/* Raises ContextLost event and updates state for lost contexts */ +void Gfx_LoseContext(const char* reason); +/* Raises ContextRecreated event and restores internal state */ +void Gfx_RecreateContext(void); +/* Attempts to restore a lost context */ +cc_bool Gfx_TryRestoreContext(void); + +/* Renders a 2D flat coloured rectangle */ +void Gfx_Draw2DFlat(int x, int y, int width, int height, PackedCol color); +/* Renders a 2D flat vertical gradient rectangle */ +void Gfx_Draw2DGradient(int x, int y, int width, int height, PackedCol top, PackedCol bottom); +/* Renders a 2D coloured texture */ +void Gfx_Draw2DTexture(const struct Texture* tex, PackedCol color); +/* Fills out the vertices for rendering a 2D coloured texture */ +void Gfx_Make2DQuad(const struct Texture* tex, PackedCol color, struct VertexTextured** vertices); + +/* Switches state to be suitable for drawing 2D graphics */ +/* NOTE: This means turning off fog/depth test, changing matrices, etc.*/ +void Gfx_Begin2D(int width, int height); +/* Switches state to be suitable for drawing 3D graphics */ +/* NOTE: This means restoring fog/depth test, restoring matrices, etc */ +void Gfx_End2D(void); + +/* Sets appropriate alpha test/blending for given block draw type */ +void Gfx_SetupAlphaState(cc_uint8 draw); +/* Undoes changes to alpha test/blending state by Gfx_SetupAlphaState */ +void Gfx_RestoreAlphaState(cc_uint8 draw); + +/* Statically initialises the position and dimensions of this texture */ +#define Tex_Rect(x,y, width,height) x,y,width,height +/* Statically initialises the texture coordinate corners of this texture */ +#define Tex_UV(u1,v1, u2,v2) { u1,v1,u2,v2 } +/* Sets the position and dimensions of this texture */ +#define Tex_SetRect(tex, xVal,yVal, wVal, hVal) tex.x = xVal; tex.y = yVal; tex.width = wVal; tex.height = hVal; +/* Sets texture coordinate corners of this texture */ +/* Useful to only draw a sub-region of the texture's pixels */ +#define Tex_SetUV(tex, U1,V1, U2,V2) tex.uv.u1 = U1; tex.uv.v1 = V1; tex.uv.u2 = U2; tex.uv.v2 = V2; + +/* Binds then renders the given texture */ +void Texture_Render(const struct Texture* tex); +/* Binds then renders the given texture */ +void Texture_RenderShaded(const struct Texture* tex, PackedCol shadeColor); +#endif diff --git a/src/Graphics_3DS.c b/src/Graphics_3DS.c new file mode 100644 index 0000000..49b5e1a --- /dev/null +++ b/src/Graphics_3DS.c @@ -0,0 +1,1056 @@ +#include "Core.h" +#if defined CC_BUILD_3DS +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include <3ds.h> +#define BUFFER_BASE_PADDR OS_VRAM_PADDR // VRAM physical address +#include "../third_party/citro3d.c" + +// autogenerated from the .v.pica files by Makefile +extern const u8 coloured_shbin[]; +extern const u32 coloured_shbin_size; + +extern const u8 textured_shbin[]; +extern const u32 textured_shbin_size; + +extern const u8 offset_shbin[]; +extern const u32 offset_shbin_size; + +#define DISPLAY_TRANSFER_FLAGS \ + (GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | GX_TRANSFER_RAW_COPY(0) | \ + GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \ + GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO)) + +static void GPUBuffers_DeleteUnreferenced(void); +static void GPUTextures_DeleteUnreferenced(void); +static cc_uint32 frameCounter1; +static PackedCol clear_color; +static cc_bool rendering3D; + + +/*########################################################################################################################* +*------------------------------------------------------Vertex shaders-----------------------------------------------------* +*#########################################################################################################################*/ +#define UNI_MVP_MATRIX (1 << 0) +#define UNI_TEX_OFFSETS (1 << 1) +// cached uniforms (cached for multiple programs) +static C3D_Mtx _mvp; +static float texOffsetX, texOffsetY; +static int texOffset; + +struct CCShader { + DVLB_s* dvlb; + shaderProgram_s program; + int uniforms; // which associated uniforms need to be resent to GPU + int locations[2]; // location of uniforms (not constant) +}; +static struct CCShader* gfx_activeShader; +static struct CCShader shaders[3]; + +static void Shader_Alloc(struct CCShader* shader, const u8* binData, int binSize) { + shader->dvlb = DVLB_ParseFile((u32*)binData, binSize); + shaderProgramInit(&shader->program); + shaderProgramSetVsh(&shader->program, &shader->dvlb->DVLE[0]); + + shader->locations[0] = shaderInstanceGetUniformLocation(shader->program.vertexShader, "MVP"); + shader->locations[1] = shaderInstanceGetUniformLocation(shader->program.vertexShader, "tex_offset"); +} + +static void Shader_Free(struct CCShader* shader) { + shaderProgramFree(&shader->program); + DVLB_Free(shader->dvlb); +} + +// Marks a uniform as changed on all programs +static void DirtyUniform(int uniform) { + for (int i = 0; i < Array_Elems(shaders); i++) + { + shaders[i].uniforms |= uniform; + } +} + +// Sends changed uniforms to the GPU for current program +static void ReloadUniforms(void) { + struct CCShader* s = gfx_activeShader; + if (!s) return; // NULL if context is lost + + if (s->uniforms & UNI_MVP_MATRIX) { + C3D_FVUnifMtx4x4(s->locations[0], &_mvp); + s->uniforms &= ~UNI_MVP_MATRIX; + } + + if (s->uniforms & UNI_TEX_OFFSETS) { + C3D_FVUnifSet(s->locations[1], + texOffsetX, texOffsetY, 0.0f, 0.0f); + s->uniforms &= ~UNI_TEX_OFFSETS; + } +} + +// Switches program to one that can render current vertex format and state +// Loads program and reloads uniforms if needed +static void SwitchProgram(void) { + struct CCShader* shader; + int index = 0; + + if (gfx_format == VERTEX_FORMAT_TEXTURED) index++; + if (texOffset) index = 2; + + shader = &shaders[index]; + if (shader != gfx_activeShader) { + gfx_activeShader = shader; + C3D_BindProgram(&shader->program); + } + ReloadUniforms(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static C3D_RenderTarget topTargetLeft; +static C3D_RenderTarget topTargetRight; +static C3D_RenderTarget bottomTarget; +static cc_bool createdTopTargetRight; +static C3D_RenderTarget* topTarget; + +static void AllocShaders(void) { + Shader_Alloc(&shaders[0], coloured_shbin, coloured_shbin_size); + Shader_Alloc(&shaders[1], textured_shbin, textured_shbin_size); + Shader_Alloc(&shaders[2], offset_shbin, offset_shbin_size); +} + +static void FreeShaders(void) { + for (int i = 0; i < Array_Elems(shaders); i++) + { + Shader_Free(&shaders[i]); + } +} + +static void SetDefaultState(void) { + Gfx_SetFaceCulling(false); + Gfx_SetAlphaTest(false); + Gfx_SetDepthWrite(true); +} + +static void InitCitro3D(void) { + C3D_Init(C3D_DEFAULT_CMDBUF_SIZE * 4); + + C3D_RenderTargetCreate(&topTargetLeft, 240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24); + C3D_RenderTargetSetOutput(&topTargetLeft, GFX_TOP, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + + // Even though the bottom screen is 320 pixels wide, we use 400 here so that the same ortho matrix + // can be used for both screens. The output is clipped to the actual screen width, anyway. + C3D_RenderTargetCreate(&bottomTarget, 240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24); + C3D_RenderTargetSetOutput(&bottomTarget, GFX_BOTTOM, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + + gfxSetDoubleBuffering(GFX_TOP, true); + SetDefaultState(); + AllocShaders(); +} + +static GfxResourceID white_square; +void Gfx_Create(void) { + if (!Gfx.Created) InitCitro3D(); + else C3Di_RenderQueueInit(); + + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.MaxTexSize = 512 * 512; + + Gfx.MinTexWidth = 8; + Gfx.MinTexHeight = 8; + Gfx.Created = true; + gfx_vsync = true; + + Gfx_RestoreState(); +} + +void Gfx_Free(void) { + Gfx_FreeState(); + C3Di_RenderQueueExit(); + gfxSet3D(false); + + // FreeShaders() + // C3D_Fini() +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 8x8 dummy white texture + // (textures must be at least 8x8, see C3D_TexInitWithParams source) + struct Bitmap bmp; + BitmapCol pixels[8*8]; + Mem_Set(pixels, 0xFF, sizeof(pixels)); + Bitmap_Init(bmp, 8, 8, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +void Gfx_3DS_SetRenderScreen(enum Screen3DS screen) { + C3D_FrameDrawOn(screen == TOP_SCREEN ? topTarget : &bottomTarget); +} + + +/*########################################################################################################################* +*----------------------------------------------------Stereoscopic support-------------------------------------------------* +*#########################################################################################################################*/ +static void Calc3DProjection(int dir, struct Matrix* proj) { + struct Matrix proj_adj = *proj; + + // See mtx_perspstereotilt + float slider = osGet3DSliderState(); + float iod = (slider / 3) * dir; + float shift = iod / (2.0f * 2.0f); + + proj_adj.row3.y = 1.0f * shift * -proj->row1.y; + Gfx.Projection = proj_adj; +} + +void Gfx_Set3DLeft(struct Matrix* proj, struct Matrix* view) { + Calc3DProjection(-1, proj); + rendering3D = true; +} + +void Gfx_Set3DRight(struct Matrix* proj, struct Matrix* view) { + Calc3DProjection(+1, proj); + + if (!createdTopTargetRight) { + C3D_RenderTargetCreate(&topTargetRight, 240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24); + C3D_RenderTargetSetOutput(&topTargetRight, GFX_TOP, GFX_RIGHT, DISPLAY_TRANSFER_FLAGS); + createdTopTargetRight = true; + } + + C3D_RenderTargetClear(&topTargetRight, C3D_CLEAR_ALL, clear_color, 0); + topTarget = &topTargetRight; + C3D_FrameDrawOn(topTarget); +} + +void Gfx_End3D(struct Matrix* proj, struct Matrix* view) { + Gfx.Projection = *proj; + + topTarget = &topTargetLeft; + C3D_FrameDrawOn(topTarget); +} + +void Gfx_SetTopRight(void) { + topTarget = &topTargetRight; + C3D_FrameDrawOn(topTarget); +} + + +/*########################################################################################################################* +*--------------------------------------------------------GPU Textures-----------------------------------------------------* +*#########################################################################################################################*/ +struct GPUTexture; +struct GPUTexture { + cc_uint32* data; + C3D_Tex texture; + struct GPUTexture* next; + cc_uint32 lastFrame; +}; +static struct GPUTexture* del_textures_head; +static struct GPUTexture* del_textures_tail; + +struct GPUTexture* GPUTexture_Alloc(void) { + struct GPUTexture* tex = Mem_AllocCleared(1, sizeof(struct GPUTexture), "GPU texture"); + return tex; +} + +// can't delete textures until not used in any frames +static void GPUTexture_Unref(GfxResourceID* resource) { + struct GPUTexture* tex = (struct GPUTexture*)(*resource); + if (!tex) return; + *resource = NULL; + + LinkedList_Append(tex, del_textures_head, del_textures_tail); +} + +static void GPUTexture_Free(struct GPUTexture* tex) { + C3D_TexDelete(&tex->texture); + Mem_Free(tex); +} + +static void GPUTextures_DeleteUnreferenced(void) { + if (!del_textures_head) return; + + struct GPUTexture* tex; + struct GPUTexture* next; + struct GPUTexture* prev = NULL; + + for (tex = del_textures_head; tex != NULL; tex = next) + { + next = tex->next; + + if (tex->lastFrame + 4 > frameCounter1) { + // texture was used within last 4 fames + prev = tex; + } else { + // advance the head of the linked list + if (del_textures_head == tex) + del_textures_head = next; + // update end of linked list if necessary + if (del_textures_tail == tex) + del_textures_tail = prev; + + // unlink this texture from the linked list + if (prev) prev->next = next; + + GPUTexture_Free(tex); + } + } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +static bool CreateNativeTexture(C3D_Tex* tex, u32 width, u32 height) { + u32 size = width * height * 4; + //tex->data = p.onVram ? vramAlloc(total_size) : linearAlloc(total_size); + tex->data = linearAlloc(size); + if (!tex->data) return false; + + tex->width = width; + tex->height = height; + tex->param = GPU_TEXTURE_MODE(GPU_TEX_2D) | + GPU_TEXTURE_MAG_FILTER(GPU_NEAREST) | GPU_TEXTURE_MIN_FILTER(GPU_NEAREST) | + GPU_TEXTURE_WRAP_S(GPU_REPEAT) | GPU_TEXTURE_WRAP_T(GPU_REPEAT); + tex->fmt = GPU_RGBA8; + tex->size = size; + + tex->border = 0; + tex->lodBias = 0; + tex->maxLevel = 0; + tex->minLevel = 0; + return true; +} + +static void TryTransferToVRAM(C3D_Tex* tex) { + return; + // NOTE: the linearFree below results in broken texture. maybe no DMA? + void* vram = vramAlloc(tex->size); + if (!vram) return; + + C3D_SyncTextureCopy((u32*)tex->data, 0, (u32*)vram, 0, tex->size, 8); + linearFree(tex->data); + tex->data = vram; +} + +/*static inline cc_uint32 CalcZOrder(cc_uint32 x, cc_uint32 y) { + // Simplified "Interleave bits by Binary Magic Numbers" from + // http://graphics.stanford.edu/~seander/bithacks.html#InterleaveTableObvious + // TODO: Simplify to array lookup? + x = (x | (x << 2)) & 0x33; + x = (x | (x << 1)) & 0x55; + + y = (y | (y << 2)) & 0x33; + y = (y | (y << 1)) & 0x55; + + return x | (y << 1); +}*/ +static inline cc_uint32 CalcZOrder(cc_uint32 a) { + // Simplified "Interleave bits by Binary Magic Numbers" from + // http://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN + // TODO: Simplify to array lookup? + a = (a | (a << 2)) & 0x33; + a = (a | (a << 1)) & 0x55; + return a; + // equivalent to return (a & 1) | ((a & 2) << 1) | (a & 4) << 2; + // but compiles to less instructions +} + +// Pixels are arranged in a recursive Z-order curve / Morton offset +// They are arranged into 8x8 tiles, where each 8x8 tile is composed of +// four 4x4 subtiles, which are in turn composed of four 2x2 subtiles +static void ToMortonTexture(C3D_Tex* tex, int originX, int originY, + struct Bitmap* bmp, int rowWidth) { + unsigned int pixel, mortonX, mortonY; + unsigned int dstX, dstY, tileX, tileY; + + int width = bmp->width, height = bmp->height; + cc_uint32* dst = tex->data; + cc_uint32* src = bmp->scan0; + + for (int y = 0; y < height; y++) + { + dstY = tex->height - 1 - (y + originY); + tileY = dstY & ~0x07; + mortonY = CalcZOrder(dstY & 0x07) << 1; + + for (int x = 0; x < width; x++) + { + dstX = x + originX; + tileX = dstX & ~0x07; + mortonX = CalcZOrder(dstX & 0x07); + pixel = src[x + (y * rowWidth)]; + + dst[(mortonX | mortonY) + (tileX * 8) + (tileY * tex->width)] = pixel; + } + } + // TODO flush data cache GSPGPU_FlushDataCache +} + + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + struct GPUTexture* tex = GPUTexture_Alloc(); + bool success = CreateNativeTexture(&tex->texture, bmp->width, bmp->height); + if (!success) return NULL; + + ToMortonTexture(&tex->texture, 0, 0, bmp, rowWidth); + if (!(flags & TEXTURE_FLAG_DYNAMIC)) TryTransferToVRAM(&tex->texture); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + struct GPUTexture* tex = (struct GPUTexture*)texId; + ToMortonTexture(&tex->texture, x, y, part, rowWidth); +} +void Gfx_DeleteTexture(GfxResourceID* texId) { + GPUTexture_Unref(texId); +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + +void Gfx_BindTexture(GfxResourceID texId) { + if (!texId) texId = white_square; + struct GPUTexture* tex = (struct GPUTexture*)texId; + + tex->lastFrame = frameCounter1; + C3D_TexBind(0, &tex->texture); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFaceCulling(cc_bool enabled) { + C3D_CullFace(enabled ? GPU_CULL_BACK_CCW : GPU_CULL_NONE); +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static void SetAlphaBlend(cc_bool enabled) { + if (enabled) { + C3D_AlphaBlend(GPU_BLEND_ADD, GPU_BLEND_ADD, GPU_SRC_ALPHA, GPU_ONE_MINUS_SRC_ALPHA, GPU_SRC_ALPHA, GPU_ONE_MINUS_SRC_ALPHA); + } else { + C3D_ColorLogicOp(GPU_LOGICOP_COPY); + } +} + +static void SetAlphaTest(cc_bool enabled) { + C3D_AlphaTest(enabled, GPU_GREATER, 0x7F); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + +void Gfx_ClearColor(PackedCol color) { + // TODO find better way? + clear_color = (PackedCol_R(color) << 24) | (PackedCol_G(color) << 16) | (PackedCol_B(color) << 8) | 0xFF; +} + +static cc_bool depthTest, depthWrite; +static int colorWriteMask = GPU_WRITE_COLOR; + +static void UpdateWriteState(void) { + //C3D_EarlyDepthTest(true, GPU_EARLYDEPTH_GREATER, 0); + //C3D_EarlyDepthTest(false, GPU_EARLYDEPTH_GREATER, 0); + int writeMask = colorWriteMask; + if (depthWrite) writeMask |= GPU_WRITE_DEPTH; + C3D_DepthTest(depthTest, GPU_GEQUAL, writeMask); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + depthWrite = enabled; + UpdateWriteState(); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + depthTest = enabled; + UpdateWriteState(); +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + int mask = 0; + if (r) mask |= GPU_WRITE_RED; + if (g) mask |= GPU_WRITE_GREEN; + if (b) mask |= GPU_WRITE_BLUE; + if (a) mask |= GPU_WRITE_ALPHA; + + colorWriteMask = mask; + UpdateWriteState(); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +static BitmapCol* _3DS_GetRow(struct Bitmap* bmp, int y, void* ctx) { + u8* fb = (u8*)ctx; + // Framebuffer is rotated 90 degrees + int width = bmp->width, height = bmp->height; + + for (int x = 0; x < width; x++) + { + int addr = (height - 1 - y + x * height) * 3; // TODO -1 or not + int b = fb[addr + 0]; + int g = fb[addr + 1]; + int r = fb[addr + 2]; + bmp->scan0[x] = BitmapColor_RGB(r, g, b); + } + return bmp->scan0; +} + +cc_result Gfx_TakeScreenshot(struct Stream* output) { + BitmapCol tmp[512]; + u16 width, height; + u8* fb = gfxGetFramebuffer(GFX_TOP, GFX_LEFT, &width, &height); + + // Framebuffer is rotated 90 degrees + struct Bitmap bmp; + bmp.scan0 = tmp; + bmp.width = height; + bmp.height = width; + + return Png_Encode(&bmp, output, _3DS_GetRow, false, fb); +} + +void Gfx_GetApiInfo(cc_string* info) { + String_Format1(info, "-- Using 3DS --\n", NULL); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { + rendering3D = false; + // wait for vblank for both screens TODO move to end? + if (gfx_vsync) C3D_FrameSync(); + + C3D_FrameBegin(0); + topTarget = &topTargetLeft; + C3D_FrameDrawOn(topTarget); + + extern void C3Di_UpdateContext(void); + C3Di_UpdateContext(); +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + int targets = 0; + if (buffers & GFX_BUFFER_COLOR) targets |= C3D_CLEAR_COLOR; + if (buffers & GFX_BUFFER_DEPTH) targets |= C3D_CLEAR_DEPTH; + + C3D_RenderTargetClear(&topTargetLeft, targets, clear_color, 0); + C3D_RenderTargetClear(&bottomTarget, targets, 0, 0); +} + +void Gfx_EndFrame(void) { + gfxSet3D(rendering3D); + C3D_FrameEnd(0); + //gfxFlushBuffers(); + //gfxSwapBuffers(); + + if (gfx_minFrameMs) LimitFPS(); + + GPUBuffers_DeleteUnreferenced(); + GPUTextures_DeleteUnreferenced(); + frameCounter1++; +} + +void Gfx_OnWindowResize(void) { } + +void Gfx_SetViewport(int x, int y, int w, int h) { } + + +/*########################################################################################################################* +*----------------------------------------------------------Buffers--------------------------------------------------------* +*#########################################################################################################################*/ +struct GPUBuffer { + cc_uint32 lastFrame; + struct GPUBuffer* next; + int pad1, pad2; + cc_uint8 data[]; // aligned to 16 bytes +}; +static struct GPUBuffer* del_buffers_head; +static struct GPUBuffer* del_buffers_tail; + +static struct GPUBuffer* GPUBuffer_Alloc(int count, int elemSize) { + void* ptr = linearAlloc(16 + count * elemSize); + return (struct GPUBuffer*)ptr; +} + +// can't delete buffers until not used in any frames +static void GPUBuffer_Unref(GfxResourceID* resource) { + struct GPUBuffer* buf = (struct GPUBuffer*)(*resource); + if (!buf) return; + *resource = NULL; + + LinkedList_Append(buf, del_buffers_head, del_buffers_tail); +} + +static void GPUBuffer_Free(struct GPUBuffer* buf) { + linearFree(buf); +} + +static void GPUBuffers_DeleteUnreferenced(void) { + if (!del_buffers_head) return; + + struct GPUBuffer* buf; + struct GPUBuffer* next; + struct GPUBuffer* prev = NULL; + + for (buf = del_buffers_head; buf != NULL; buf = next) + { + next = buf->next; + + if (buf->lastFrame + 4 > frameCounter1) { + // texture was used within last 4 fames + prev = buf; + } else { + // advance the head of the linked list + if (del_buffers_head == buf) + del_buffers_head = next; + // update end of linked list if necessary + if (del_buffers_tail == buf) + del_buffers_tail = prev; + + // unlink this texture from the linked list + if (prev) prev->next = next; + + GPUBuffer_Free(buf); + } + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint16* gfx_indices; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + if (!gfx_indices) { + gfx_indices = linearAlloc(count * sizeof(cc_uint16)); + if (!gfx_indices) Logger_Abort("Failed to allocate memory for index buffer"); + } + + fillFunc(gfx_indices, count, obj); + return gfx_indices; +} + +void Gfx_BindIb(GfxResourceID ib) { + u32 pa = osConvertVirtToPhys(ib); + GPUCMD_AddWrite(GPUREG_INDEXBUFFER_CONFIG, (pa - BUFFER_BASE_PADDR) | (C3D_UNSIGNED_SHORT << 31)); +} + +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*-------------------------------------------------------Vertex buffers----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint8* gfx_vertices; + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return GPUBuffer_Alloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + buffer->lastFrame = frameCounter1; + gfx_vertices = buffer->data; +} + +void Gfx_DeleteVb(GfxResourceID* vb) { GPUBuffer_Unref(vb); } + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + return buffer->data; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + gfx_vertices = buffer->data; +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return GPUBuffer_Alloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + return buffer->data; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + gfx_vertices = buffer->data; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static u32 fogColor; +static C3D_FogLut fog_lut; +static int fogMode = FOG_LINEAR; +static float fogDensity = 1.0f; +static float fogEnd = 32.0f; + +void Gfx_SetFog(cc_bool enabled) { + C3D_FogGasMode(enabled ? GPU_FOG : GPU_NO_FOG, GPU_PLAIN_DENSITY, false); + // TODO doesn't work quite right +} + +void Gfx_SetFogCol(PackedCol color) { + // TODO find better way? + u32 c = (0xFFu << 24) | (PackedCol_B(color) << 16) | (PackedCol_G(color) << 8) | PackedCol_R(color); + if (c == fogColor) return; + + fogColor = c; + C3D_FogColor(c); +} + +static void ApplyFog(float* values) { + float data[256]; + + for (int i = 0; i <= 128; i ++) + { + float val = values[i]; + if (i < 128) data[i] = val; + if (i > 0) data[i + 127] = val - data[i-1]; + } + + FogLut_FromArray(&fog_lut, data); + C3D_FogLutBind(&fog_lut); +} + +static float GetFogValue(float c) { + if (fogMode == FOG_LINEAR) { + return (fogEnd - c) / fogEnd; + } else if (fogMode == FOG_EXP) { + return expf(-(fogDensity * c)); + } else { + return expf(-(fogDensity * c) * (fogDensity * c)); + } +} + +static void UpdateFog(void) { + float near = 0.01f; + float far = Game_ViewDistance; + float values[129]; + + // TODO: Exp calculation isn't right for lava ??? + for (int i = 0; i <= 128; i ++) + { + float c = FogLut_CalcZ(i / 128.0f, near, far); + values[i] = GetFogValue(c); + } + ApplyFog(values); +} + +void Gfx_SetFogDensity(float value) { + if (fogDensity == value) return; + + fogDensity = value; + UpdateFog(); +} + +void Gfx_SetFogEnd(float value) { + if (fogEnd == value) return; + + fogEnd = value; + UpdateFog(); +} + +void Gfx_SetFogMode(FogFunc func) { + fogMode = func; + UpdateFog(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static C3D_Mtx _view, _proj; + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // See Mtx_OrthoTilt in Citro3D for the original basis + // (it's mostly just a standard orthograph matrix rotated by 90 degrees) + // Note: The rows/columns are "flipped" over the diagonal axis compared to original basis + Mem_Set(matrix, 0, sizeof(struct Matrix)); + + matrix->row2.x = -2.0f / height; + matrix->row4.x = 1.0f; + matrix->row1.y = -2.0f / width; + matrix->row4.y = 1.0f; + + matrix->row3.z = 1.0f / (zNear - zFar); + matrix->row4.z = 0.5f * (zNear + zFar) / (zNear - zFar) - 0.5f; + matrix->row4.w = 1.0f; +} + +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + // See Mtx_PerspTilt in Citro3D for the original basis + // (it's mostly just a standard perspective matrix rotated by 90 degrees) + // Note: The rows/columns are "flipped" over the diagonal axis compared to original basis + float zNear = 0.1f; + fov = tanf(fov / 2.0f); + Mem_Set(matrix, 0, sizeof(struct Matrix)); + + matrix->row2.x = 1.0f / fov; + matrix->row1.y = -1.0f / (fov * aspect); + matrix->row4.z = zFar * zNear / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row3.z = 1.0f * zNear / (zNear - zFar); +} + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + C3D_Mtx* dst = type == MATRIX_VIEW ? &_view : &_proj; + float* src = (float*)matrix; + + // Transpose + for (int i = 0; i < 4; i++) + { + dst->r[i].x = src[0 + i]; + dst->r[i].y = src[4 + i]; + dst->r[i].z = src[8 + i]; + dst->r[i].w = src[12 + i]; + } + + Mtx_Multiply(&_mvp, &_proj, &_view); + DirtyUniform(UNI_MVP_MATRIX); + ReloadUniforms(); +} + + +/*void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + + // Provided projection matrix assumes landscape display, but 3DS framebuffer + // is a rotated portrait display, so need to swap pixel X/Y values to correct that + // + // This can be done by rotating the projection matrix 90 degrees around Z axis + // https://open.gl/transformations + if (type == MATRIX_PROJECTION) { + struct Matrix rot = Matrix_Identity; + rot.row1.x = 0; rot.row1.y = 1; + rot.row2.x = -1; rot.row2.y = 0; + //Matrix_RotateZ(&rot, 90 * MATH_DEG2RAD); + //Matrix_Mul(&_proj, &_proj, &rot); // TODO avoid Matrix_Mul ?? + Matrix_Mul(&_proj, matrix, &rot); // TODO avoid Matrix_Mul ? + } + + UpdateMVP(); + DirtyUniform(UNI_MVP_MATRIX); + ReloadUniforms(); +}*/ + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + texOffset = true; + texOffsetX = x; + texOffsetY = y; + + shaders[2].uniforms |= UNI_TEX_OFFSETS; + SwitchProgram(); +} +void Gfx_DisableTextureOffset(void) { + texOffset = false; + SwitchProgram(); +} + + + +/*########################################################################################################################* +*---------------------------------------------------------Drawing---------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +static void UpdateAttribFormat(VertexFormat fmt) { + C3D_AttrInfo* attrInfo = C3D_GetAttrInfo(); + AttrInfo_Init(attrInfo); + + AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // in_pos + AttrInfo_AddLoader(attrInfo, 1, GPU_UNSIGNED_BYTE, 4); // in_col + if (fmt == VERTEX_FORMAT_TEXTURED) { + AttrInfo_AddLoader(attrInfo, 2, GPU_FLOAT, 2); // in_tex + } +} + +static void UpdateTexEnv(VertexFormat fmt) { + int func, source; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + // Configure the first fragment shading substage to blend the texture color with + // the vertex color (calculated by the vertex shader using a lighting algorithm) + // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight + source = GPU_TEVSOURCES(GPU_TEXTURE0, GPU_PRIMARY_COLOR, 0); + func = GPU_MODULATE; + } else { + // Configure the first fragment shading substage to just pass through the vertex color + // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight + source = GPU_TEVSOURCES(GPU_PRIMARY_COLOR, 0, 0); + func = GPU_REPLACE; + } + + GPUCMD_AddWrite(GPUREG_TEXENV0_SOURCE, source | (source << 16)); + GPUCMD_AddWrite(GPUREG_TEXENV0_COMBINER, func | (func << 16)); +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + SwitchProgram(); + UpdateAttribFormat(fmt); + UpdateTexEnv(fmt); +} + +void Gfx_DrawVb_Lines(int verticesCount) { + /* TODO */ +} + +static void SetVertexSource(int startVertex) { + // https://github.com/devkitPro/citro3d/issues/47 + // "Fyi the permutation specifies the order in which the attributes are stored in the buffer, LSB first. So 0x210 indicates attributes 0, 1 & 2." + const void* data; + int stride, attribs, permutation; + + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + data = (struct VertexTextured*)gfx_vertices + startVertex; + stride = SIZEOF_VERTEX_TEXTURED; + attribs = 3; + permutation = 0x210; + } else { + data = (struct VertexColoured*)gfx_vertices + startVertex; + stride = SIZEOF_VERTEX_COLOURED; + attribs = 2; + permutation = 0x10; + } + + u32 pa = osConvertVirtToPhys(data); + u32 args[3]; // GPUREG_ATTRIBBUFFER0_OFFSET, GPUREG_ATTRIBBUFFER0_CONFIG1, GPUREG_ATTRIBBUFFER0_CONFIG2 + + args[0] = pa - BUFFER_BASE_PADDR; + args[1] = permutation; + args[2] = (stride << 16) | (attribs << 28); + + GPUCMD_AddIncrementalWrites(GPUREG_ATTRIBBUFFER0_OFFSET, args, 3); + // NOTE: Can't use GPUREG_VERTEX_OFFSET, it only works when drawing non-indexed arrays +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + SetVertexSource(startVertex); + C3D_DrawElements(GPU_TRIANGLES, ICOUNT(verticesCount)); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + SetVertexSource(0); + C3D_DrawElements(GPU_TRIANGLES, ICOUNT(verticesCount)); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + SetVertexSource(startVertex); + C3D_DrawElements(GPU_TRIANGLES, ICOUNT(verticesCount)); +} + +// TODO: TEMP HACK !! +void Gfx_Draw2DFlat(int x, int y, int width, int height, PackedCol color) { + struct VertexColoured v1, v2, v3, v4; + v1.x = (float)x; v1.y = (float)y; + v2.x = (float)(x + width); v2.y = (float)y; + v3.x = (float)(x + width); v3.y = (float)(y + height); + v4.x = (float)x; v4.y = (float)(y + height); + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + C3D_ImmDrawBegin(GPU_TRIANGLES); + C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v2.x, v2.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v4.x, v4.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmDrawEnd(); +} + +void Gfx_Draw2DGradient(int x, int y, int width, int height, PackedCol top, PackedCol bottom) { + struct VertexColoured v1, v2, v3, v4; + v1.x = (float)x; v1.y = (float)y; + v2.x = (float)(x + width); v2.y = (float)y; + v3.x = (float)(x + width); v3.y = (float)(y + height); + v4.x = (float)x; v4.y = (float)(y + height); + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + C3D_ImmDrawBegin(GPU_TRIANGLES); + C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(top), PackedCol_G(top), PackedCol_B(top), PackedCol_A(top)); + C3D_ImmSendAttrib(v2.x, v2.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(top), PackedCol_G(top), PackedCol_B(top), PackedCol_A(top)); + C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(bottom), PackedCol_G(bottom), PackedCol_B(bottom), PackedCol_A(bottom)); + C3D_ImmSendAttrib(v3.x, v3.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(bottom), PackedCol_G(bottom), PackedCol_B(bottom), PackedCol_A(bottom)); + C3D_ImmSendAttrib(v4.x, v4.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(bottom), PackedCol_G(bottom), PackedCol_B(bottom), PackedCol_A(bottom)); + C3D_ImmSendAttrib(v1.x, v1.y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(top), PackedCol_G(top), PackedCol_B(top), PackedCol_A(top)); + C3D_ImmDrawEnd(); +} + +void Gfx_Draw2DTexture(const struct Texture* tex, PackedCol color) { + struct VertexTextured v[4]; + struct VertexTextured* ptr = v; + Gfx_Make2DQuad(tex, color, &ptr); + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + C3D_ImmDrawBegin(GPU_TRIANGLES); + C3D_ImmSendAttrib(v[0].x, v[0].y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v[0].U, v[0].V, 0.0f, 0.0f); + C3D_ImmSendAttrib(v[1].x, v[1].y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v[1].U, v[1].V, 0.0f, 0.0f); + C3D_ImmSendAttrib(v[2].x, v[2].y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v[2].U, v[2].V, 0.0f, 0.0f); + C3D_ImmSendAttrib(v[2].x, v[2].y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v[2].U, v[2].V, 0.0f, 0.0f); + C3D_ImmSendAttrib(v[3].x, v[3].y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v[3].U, v[3].V, 0.0f, 0.0f); + C3D_ImmSendAttrib(v[0].x, v[0].y, 0.0f, 1.0f); + C3D_ImmSendAttrib(PackedCol_R(color), PackedCol_G(color), PackedCol_B(color), PackedCol_A(color)); + C3D_ImmSendAttrib(v[0].U, v[0].V, 0.0f, 0.0f); + C3D_ImmDrawEnd(); +} +#endif diff --git a/src/Graphics_D3D11.c b/src/Graphics_D3D11.c new file mode 100644 index 0000000..1b06591 --- /dev/null +++ b/src/Graphics_D3D11.c @@ -0,0 +1,1209 @@ +#include "Core.h" +#if CC_GFX_BACKEND == CC_GFX_BACKEND_D3D11 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#include "../misc/windows/D3D11Shaders.h" + +/* Avoid pointless includes */ +#define WIN32_LEAN_AND_MEAN +#define NOSERVICE +#define NOMCX +#define NOIME +#define COBJMACROS +#include +static const GUID guid_ID3D11Texture2D = { 0x6f15aaf2, 0xd208, 0x4e89, { 0x9a, 0xb4, 0x48, 0x95, 0x35, 0xd3, 0x4f, 0x9c } }; +static const GUID guid_IXDGIDevice = { 0x54ec77fa, 0x1377, 0x44e6, { 0x8c, 0x32, 0x88, 0xfd, 0x5f, 0x44, 0xc8, 0x4c } }; + +// some generally useful debugging links +// https://docs.microsoft.com/en-us/visualstudio/debugger/graphics/visual-studio-graphics-diagnostics +// https://stackoverflow.com/questions/50591937/how-do-i-launch-the-hlsl-debugger-in-visual-studio-2017 +// https://docs.microsoft.com/en-us/visualstudio/debugger/graphics/graphics-object-table?view=vs-2019 +// https://github.com/gfx-rs/wgpu/issues/1171 +// Some generally useful background links +// https://gist.github.com/d7samurai/261c69490cce0620d0bfc93003cd1052 + +static int depthBits; // TODO implement depthBits?? for ZNear calc +static GfxResourceID white_square; + +#ifdef _MSC_VER +#define CC_ALIGNED(x) __declspec(align(x)) +#else +#define CC_ALIGNED(x) __attribute__((aligned(x))) +#endif + +static ID3D11Device* device; +static ID3D11DeviceContext* context; +static IDXGIDevice1* dxgi_device; +static IDXGIAdapter* dxgi_adapter; +static IDXGIFactory1* dxgi_factory; +static IDXGISwapChain* swapchain; +struct ShaderDesc { const void* data; int len; }; + +static void IA_UpdateLayout(void); +static void VS_UpdateShader(void); +static void PS_UpdateShader(void); +static void InitPipeline(void); +static void FreePipeline(void); + +static PFN_D3D11_CREATE_DEVICE_AND_SWAP_CHAIN _D3D11CreateDeviceAndSwapChain; + +static void LoadD3D11Library(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(D3D11CreateDeviceAndSwapChain) + }; + static const cc_string path = String_FromConst("d3d11.dll"); + void* lib; + DynamicLib_LoadAll(&path, funcs, Array_Elems(funcs), &lib); + + if (lib) return; + Logger_FailToStart("Failed to load d3d11.dll. You may need to install Direct3D11.\n\nNOTE: Direct3D11 requires Windows 7 or later\nYou may need to use the Direct3D9 version instead.\n"); +} + +static void CreateDeviceAndSwapChain(void) { + // https://docs.microsoft.com/en-us/windows/uwp/gaming/simple-port-from-direct3d-9-to-11-1-part-1--initializing-direct3d + DWORD createFlags = 0; + D3D_FEATURE_LEVEL fl; + HRESULT hr; +#ifdef _DEBUG + createFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + DXGI_SWAP_CHAIN_DESC desc = { 0 }; + desc.BufferCount = 1; + desc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + // RefreshRate intentionally left at 0 so display's refresh rate is used + desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + desc.OutputWindow = Window_Main.Handle; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Windowed = TRUE; + + hr = _D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, + createFlags, NULL, 0, D3D11_SDK_VERSION, + &desc, &swapchain, &device, &fl, &context); + if (hr) Logger_Abort2(hr, "Failed to create D3D11 device"); + + // The fog calculation requires reading Z/W of fragment position (SV_POSITION) in pixel shader, + // unfortunately this is unsupported in Direct3d9 (https://docs.microsoft.com/en-us/windows/uwp/gaming/glsl-to-hlsl-reference) + // So for the sake of simplicity and since only a few old GPUs don't support feature level 10 anyways + // https://walbourn.github.io/direct3d-feature-levels/ + // https://github.com/MonoGame/MonoGame/issues/5789 + // I decided to just not support GPUs that do not support at least feature level 10 + if (fl < D3D_FEATURE_LEVEL_10_0) + Logger_FailToStart("Your GPU is too old to support the Direct3D11 version.\nTry using the Direct3D9 version instead.\n"); + + // https://docs.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_texture2d_desc + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits + // see "Max Texture Dimension" row in Feature Support table + Gfx.MaxTexWidth = fl < D3D_FEATURE_LEVEL_11_0 ? 8192 : 16384; + Gfx.MaxTexHeight = fl < D3D_FEATURE_LEVEL_11_0 ? 8192 : 16384; +} + +void Gfx_Create(void) { + LoadD3D11Library(); + CreateDeviceAndSwapChain(); + Gfx.Created = true; + customMipmapsLevels = true; + Gfx_RestoreState(); +} + +void Gfx_Free(void) { + Gfx_FreeState(); + IDXGISwapChain_Release(swapchain); + ID3D11DeviceContext_Release(context); + +#ifndef _DEBUG + ID3D11Device_Release(device); +#else + ULONG refCount = ID3D11Device_Release(device); + if (refCount == 0) return; // device destroyed with no issues + + ID3D11Debug *d3dDebug; + static const GUID guid_d3dDebug = { 0x79cf2233, 0x7536, 0x4948,{ 0x9d, 0x36, 0x1e, 0x46, 0x92, 0xdc, 0x57, 0x60 } }; + HRESULT hr = ID3D11Device_QueryInterface(device, &guid_d3dDebug, &d3dDebug); + if (SUCCEEDED(hr)) + { + hr = ID3D11Debug_ReportLiveDeviceObjects(d3dDebug, D3D11_RLDO_DETAIL); + } +#endif +} + +static cc_bool inited; +cc_bool Gfx_TryRestoreContext(void) { + // https://docs.microsoft.com/en-us/windows/uwp/gaming/handling-device-lost-scenarios + Gfx_Free(); + Gfx_Create(); + return true; +} + +static void Gfx_FreeState(void) { + if (!inited) return; + inited = false; + + FreeDefaultResources(); + FreePipeline(); + Gfx_DeleteTexture(&white_square); +} + +static void Gfx_RestoreState(void) { + if (inited) return; + inited = true; + + InitDefaultResources(); + gfx_format = -1; + InitPipeline(); + + /* 1x1 dummy white texture */ + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + Gfx_RecreateTexture(&white_square, &bmp, 0, false); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +static void D3D11_DoMipmaps(ID3D11Resource* texture, int x, int y, struct Bitmap* bmp, int rowWidth) { + BitmapCol* prev = bmp->scan0; + BitmapCol* cur; + + int lvls = CalcMipmapsLevels(bmp->width, bmp->height); + int lvl, width = bmp->width, height = bmp->height; + + for (lvl = 1; lvl <= lvls; lvl++) + { + x /= 2; y /= 2; + if (width > 1) width /= 2; + if (height > 1) height /= 2; + + cur = (BitmapCol*)Mem_Alloc(width * height, 4, "mipmaps"); + GenMipmaps(width, height, cur, prev, rowWidth); + + D3D11_BOX box; + box.front = 0; + box.back = 1; + box.left = x; + box.right = x + width; + box.top = y; + box.bottom = y + height; + + // https://eatplayhate.me/2013/09/29/d3d11-texture-update-costs/ + // Might not be ideal, but seems to work well enough + int stride = width * 4; + ID3D11DeviceContext_UpdateSubresource(context, texture, lvl, &box, cur, stride, stride * height); + + if (prev != bmp->scan0) Mem_Free(prev); + prev = cur; + rowWidth = width; + } + if (prev != bmp->scan0) Mem_Free(prev); +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + ID3D11Texture2D* tex = NULL; + ID3D11ShaderResourceView* view = NULL; + HRESULT hr; + + D3D11_TEXTURE2D_DESC desc = { 0 }; + desc.Width = bmp->width; + desc.Height = bmp->height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.Usage = D3D11_USAGE_DEFAULT; // TODO D3D11_USAGE_IMMUTABLE when not dynamic + desc.SampleDesc.Count = 1; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + D3D11_SUBRESOURCE_DATA data; + data.pSysMem = bmp->scan0; + data.SysMemPitch = rowWidth * 4; + data.SysMemSlicePitch = 0; + D3D11_SUBRESOURCE_DATA* src = &data; + + // Direct3D11 specifies pInitialData as an array of D3D11_SUBRESOURCE_DATA of length desc->MipsLevels + // Rather than writing such code just to support the mipmaps case, I went with the simpler approach of + // leaving pInitialData as NULL and specfiying the texture data later using Gfx_UpdateTexturePart + if (mipmaps) { + desc.MipLevels += CalcMipmapsLevels(bmp->width, bmp->height); + src = NULL; + } + + while ((hr = ID3D11Device_CreateTexture2D(device, &desc, src, &tex))) + { + if (hr == E_OUTOFMEMORY) { + // insufficient VRAM or RAM left to allocate texture, try to reduce the memory in use first + // if can't reduce, return 'empty' texture so that at least the game will continue running + if (!Game_ReduceVRAM()) return 0; + } else { + // unknown issue, so don't even try to handle the error + Logger_Abort2(hr, "Failed to create texture"); + } + } + + hr = ID3D11Device_CreateShaderResourceView(device, tex, NULL, &view); + if (hr) Logger_Abort2(hr, "Failed to create view"); + + if (mipmaps) Gfx_UpdateTexture(view, 0, 0, bmp, rowWidth, mipmaps); + return view; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + ID3D11ShaderResourceView* view = (ID3D11ShaderResourceView*)texId; + ID3D11Resource* res = NULL; + D3D11_BOX box; + box.front = 0; + box.back = 1; + box.left = x; + box.right = x + part->width; + box.top = y; + box.bottom = y + part->height; + + // https://eatplayhate.me/2013/09/29/d3d11-texture-update-costs/ + // Might not be ideal, but seems to work well enough + int stride = rowWidth * 4; + ID3D11ShaderResourceView_GetResource(view, &res); + ID3D11DeviceContext_UpdateSubresource(context, res, 0, &box, part->scan0, stride, stride * part->height); + + if (mipmaps) D3D11_DoMipmaps(res, x, y, part, rowWidth); + ID3D11Resource_Release(res); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + ID3D11ShaderResourceView* view = (ID3D11ShaderResourceView*)(*texId); + ID3D11Resource* res = NULL; + + if (view) { + ID3D11ShaderResourceView_GetResource(view, &res); + ID3D11Resource_Release(res); + ID3D11ShaderResourceView_Release(view); + + // note that ID3D11ShaderResourceView_GetResource increments refcount, so need to Release twice + // https://docs.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11view-getresource + ID3D11Resource_Release(res); + } + *texId = NULL; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + ID3D11Buffer* buffer = NULL; + cc_uint16 indices[GFX_MAX_INDICES]; + fillFunc(indices, count, obj); + + D3D11_BUFFER_DESC desc = { 0 }; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.ByteWidth = count * sizeof(cc_uint16); + desc.BindFlags = D3D11_BIND_INDEX_BUFFER; + + D3D11_SUBRESOURCE_DATA data; + data.pSysMem = indices; + data.SysMemPitch = 0; + data.SysMemSlicePitch = 0; + + HRESULT hr = ID3D11Device_CreateBuffer(device, &desc, &data, &buffer); + if (hr) Logger_Abort2(hr, "Failed to create index buffer"); + return buffer; +} + +void Gfx_DeleteIb(GfxResourceID* ib) { + ID3D11Buffer* buffer = (ID3D11Buffer*)(*ib); + if (buffer) ID3D11Buffer_Release(buffer); + *ib = NULL; +} + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static ID3D11Buffer* CreateVertexBuffer(VertexFormat fmt, int count, cc_bool dynamic) { + ID3D11Buffer* buffer = NULL; + + D3D11_BUFFER_DESC desc = { 0 }; + desc.Usage = dynamic ? D3D11_USAGE_DYNAMIC : D3D11_USAGE_DEFAULT; + desc.CPUAccessFlags = dynamic ? D3D11_CPU_ACCESS_WRITE : 0; + desc.ByteWidth = count * strideSizes[fmt]; + desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + /* TODO set data initially */ + + HRESULT hr = ID3D11Device_CreateBuffer(device, &desc, NULL, &buffer); + if (hr) Logger_Abort2(hr, "Failed to create vertex buffer"); + return buffer; +} + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + /* TODO immutable? */ + return CreateVertexBuffer(fmt, count, false); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { + ID3D11Buffer* buffer = (ID3D11Buffer*)(*vb); + if (buffer) ID3D11Buffer_Release(buffer); + *vb = NULL; +} + +static void* tmp; +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + tmp = Mem_TryAlloc(count, strideSizes[fmt]); + return tmp; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + ID3D11Buffer* buffer = (ID3D11Buffer*)vb; + ID3D11DeviceContext_UpdateSubresource(context, buffer, 0, NULL, tmp, 0, 0); + Mem_Free(tmp); + tmp = NULL; +} + + +/*########################################################################################################################* +*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return CreateVertexBuffer(fmt, maxVertices, true); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { + ID3D11Buffer* buffer = (ID3D11Buffer*)(*vb); + if (buffer) ID3D11Buffer_Release(buffer); + *vb = NULL; +} + +static D3D11_MAPPED_SUBRESOURCE mapDesc; +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + ID3D11Buffer* buffer = (ID3D11Buffer*)vb; + mapDesc.pData = NULL; + + HRESULT hr = ID3D11DeviceContext_Map(context, buffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapDesc); + if (hr) Logger_Abort2(hr, "Failed to lock dynamic VB"); + return mapDesc.pData; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + ID3D11Buffer* buffer = (ID3D11Buffer*)vb; + ID3D11DeviceContext_Unmap(context, buffer, 0); + Gfx_BindDynamicVb(vb); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Vertex rendering----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + IA_UpdateLayout(); + VS_UpdateShader(); + PS_UpdateShader(); +} + +void Gfx_DrawVb_Lines(int verticesCount) { + ID3D11DeviceContext_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY_LINELIST); + ID3D11DeviceContext_Draw(context, verticesCount, 0); + ID3D11DeviceContext_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + ID3D11DeviceContext_DrawIndexed(context, ICOUNT(verticesCount), 0, 0); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + ID3D11DeviceContext_DrawIndexed(context, ICOUNT(verticesCount), 0, startVertex); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + ID3D11DeviceContext_DrawIndexed(context, ICOUNT(verticesCount), 0, startVertex); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh + // The simplified calculation below uses: L = 0, R = width, T = 0, B = height + // NOTE: This calculation is shared with Direct3D 9 backend + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + // Deliberately swap zNear/zFar in projection matrix calculation to produce + // a projection matrix that results in a reversed depth buffer + // https://developer.nvidia.com/content/depth-precision-visualized + float zNear_ = zFar; + float zFar_ = Reversed_CalcZNear(fov, 24); // TODO don't always hardcode to 24 bits + + // Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh + // NOTE: This calculation is shared with Direct3D 9 backend + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar_ / (zNear_ - zFar_); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear_ * zFar_) / (zNear_ - zFar_); + matrix->row4.w = 0.0f; +} + +//#####################z################################################################################################### +//-------------------------------------------------------Input Assembler-------------------------------------------------- +//######################################################################################################################## +// https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-input-assembler-stage +static ID3D11InputLayout* input_textured; + +static void IA_CreateLayouts(void) { + ID3D11InputLayout* input = NULL; + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-input-assembler-stage-getting-started + // https://docs.microsoft.com/en-us/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-legacy-formats + // https://stackoverflow.com/questions/23398711/d3d11-input-element-desc-element-types-ordering-packing + // D3D11_APPEND_ALIGNED_ELEMENT + static D3D11_INPUT_ELEMENT_DESC T_layout[] = + { + { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "COLOR" , 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + }; + HRESULT hr = ID3D11Device_CreateInputLayout(device, T_layout, Array_Elems(T_layout), + vs_textured, sizeof(vs_textured), &input); + input_textured = input; +} + +static void IA_UpdateLayout(void) { + ID3D11DeviceContext_IASetInputLayout(context, input_textured); +} + +static void IA_Init(void) { + IA_CreateLayouts(); + ID3D11DeviceContext_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); +} + +static void IA_Free(void) { + ID3D11InputLayout_Release(input_textured); +} + +void Gfx_BindIb(GfxResourceID ib) { + ID3D11Buffer* buffer = (ID3D11Buffer*)ib; + ID3D11DeviceContext_IASetIndexBuffer(context, buffer, DXGI_FORMAT_R16_UINT, 0); +} + +void Gfx_BindVb(GfxResourceID vb) { + ID3D11Buffer* buffer = (ID3D11Buffer*)vb; + static UINT32 offset[] = { 0 }; + ID3D11DeviceContext_IASetVertexBuffers(context, 0, 1, &buffer, &gfx_stride, offset); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { + ID3D11Buffer* buffer = (ID3D11Buffer*)vb; + static UINT32 offset[] = { 0 }; + ID3D11DeviceContext_IASetVertexBuffers(context, 0, 1, &buffer, &gfx_stride, offset); +} + + +//######################################################################################################################## +//--------------------------------------------------------Vertex shader--------------------------------------------------- +//######################################################################################################################## +// https://docs.microsoft.com/en-us/windows/win32/direct3d11/vertex-shader-stage +static ID3D11VertexShader* vs_shaders[3]; +static ID3D11Buffer* vs_cBuffer; + +static struct CC_ALIGNED(64) VSConstants { + struct Matrix mvp; + float texX, texY; +} vs_constants; +static const struct ShaderDesc vs_descs[] = { + { vs_colored, sizeof(vs_colored) }, + { vs_textured, sizeof(vs_textured) }, + { vs_textured_offset, sizeof(vs_textured_offset) }, +}; + +static void VS_CreateShaders(void) { + for (int i = 0; i < Array_Elems(vs_shaders); i++) + { + HRESULT hr = ID3D11Device_CreateVertexShader(device, vs_descs[i].data, vs_descs[i].len, NULL, &vs_shaders[i]); + if (hr) Logger_Abort2(hr, "Failed to compile vertex shader"); + } +} + +static void VS_CreateConstants(void) { + // https://developer.nvidia.com/content/constant-buffers-without-constant-pain-0 + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-buffers-constant-how-to + // https://gamedev.stackexchange.com/questions/18026/directx11-how-do-i-manage-and-update-multiple-shader-constant-buffers + D3D11_BUFFER_DESC desc = { 0 }; + desc.ByteWidth = sizeof(vs_constants); + //desc.Usage = D3D11_USAGE_DYNAMIC; + //desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + //desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + + D3D11_SUBRESOURCE_DATA data; + data.pSysMem = &vs_constants; + data.SysMemPitch = 0; + data.SysMemSlicePitch = 0; + + HRESULT hr = ID3D11Device_CreateBuffer(device, &desc, &data, &vs_cBuffer); + ID3D11DeviceContext_VSSetConstantBuffers(context, 0, 1, &vs_cBuffer); +} + +static int VS_CalcShaderIndex(void) { + if (gfx_format == VERTEX_FORMAT_COLOURED) return 0; + + cc_bool has_offset = vs_constants.texX != 0 || vs_constants.texY != 0; + return has_offset ? 2 : 1; +} + +static void VS_UpdateShader(void) { + int idx = VS_CalcShaderIndex(); + ID3D11DeviceContext_VSSetShader(context, vs_shaders[idx], NULL, 0); +} + +static void VS_FreeShaders(void) { + for (int i = 0; i < Array_Elems(vs_shaders); i++) + { + ID3D11VertexShader_Release(vs_shaders[i]); + } +} + +static void VS_UpdateConstants(void) { + ID3D11DeviceContext_UpdateSubresource(context, vs_cBuffer, 0, NULL, &vs_constants, 0, 0); +} + +static void VS_FreeConstants(void) { + ID3D11Buffer_Release(vs_cBuffer); +} + +static void VS_Init(void) { + VS_CreateShaders(); + VS_CreateConstants(); + VS_UpdateShader(); +} + +static void VS_Free(void) { + VS_FreeShaders(); + VS_FreeConstants(); +} + +static struct Matrix _view, _proj; +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + Matrix_Mul(&vs_constants.mvp, &_view, &_proj); + VS_UpdateConstants(); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + vs_constants.texX = x; + vs_constants.texY = y; + VS_UpdateShader(); + VS_UpdateConstants(); +} + +void Gfx_DisableTextureOffset(void) { + vs_constants.texX = 0; + vs_constants.texY = 0; + VS_UpdateShader(); +} + + +//######################################################################################################################## +//---------------------------------------------------------Rasteriser----------------------------------------------------- +//######################################################################################################################## +// https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-rasterizer-stage +static ID3D11RasterizerState* rs_states[2]; +static cc_bool rs_culling; + +static void RS_CreateRasterState(void) { + // https://docs.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_rasterizer_desc + D3D11_RASTERIZER_DESC desc = { 0 }; + desc.CullMode = D3D11_CULL_NONE; + desc.FillMode = D3D11_FILL_SOLID; + desc.FrontCounterClockwise = true; + desc.DepthClipEnable = true; // otherwise vertices/pixels beyond far plane are still wrongly rendered + ID3D11Device_CreateRasterizerState(device, &desc, &rs_states[0]); + + desc.CullMode = D3D11_CULL_BACK; + ID3D11Device_CreateRasterizerState(device, &desc, &rs_states[1]); +} + +void Gfx_SetViewport(int x, int y, int w, int h) { + D3D11_VIEWPORT viewport; + viewport.TopLeftX = x; + viewport.TopLeftY = y; + viewport.Width = w; + viewport.Height = h; + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + ID3D11DeviceContext_RSSetViewports(context, 1, &viewport); +} + +static void RS_UpdateRasterState(void) { + ID3D11DeviceContext_RSSetState(context, rs_states[rs_culling]); +} + +static void RS_FreeRasterStates(void) { + for (int i = 0; i < Array_Elems(rs_states); i++) + { + ID3D11RasterizerState_Release(rs_states[i]); + } +} + +static void RS_Init(void) { + RS_CreateRasterState(); + Gfx_SetViewport(0, 0, Game.Width, Game.Height); + RS_UpdateRasterState(); +} + +static void RS_Free(void) { + RS_FreeRasterStates(); +} + +void Gfx_SetFaceCulling(cc_bool enabled) { + rs_culling = enabled; + RS_UpdateRasterState(); +} + + +//######################################################################################################################## +//--------------------------------------------------------Pixel shader---------------------------------------------------- +//######################################################################################################################## +// https://docs.microsoft.com/en-us/windows/win32/direct3d11/pixel-shader-stage +static ID3D11SamplerState* ps_samplers[2]; +static ID3D11PixelShader* ps_shaders[12]; +static ID3D11Buffer* ps_cBuffer; +static cc_bool ps_mipmaps; +static float ps_fogEnd, ps_fogDensity; +static PackedCol ps_fogColor; +static int ps_fogMode; + +static struct CC_ALIGNED(64) PSConstants { + float fogValue; + float fogR, fogG, fogB; +} ps_constants; +static const struct ShaderDesc ps_descs[] = { + { ps_colored, sizeof(ps_colored) }, + { ps_textured, sizeof(ps_textured) }, + { ps_colored_test, sizeof(ps_colored_test) }, + { ps_textured_test, sizeof(ps_textured_test) }, + { ps_colored_linear, sizeof(ps_colored_linear) }, + { ps_textured_linear, sizeof(ps_textured_linear) }, + { ps_colored_test_linear, sizeof(ps_colored_test_linear) }, + { ps_textured_test_linear, sizeof(ps_textured_test_linear) }, + { ps_colored_density, sizeof(ps_colored_density) }, + { ps_textured_density, sizeof(ps_textured_density) }, + { ps_colored_test_density, sizeof(ps_colored_test_density) }, + { ps_textured_test_density, sizeof(ps_textured_test_density) }, +}; + +static void PS_CreateShaders(void) { + for (int i = 0; i < Array_Elems(ps_shaders); i++) + { + HRESULT hr = ID3D11Device_CreatePixelShader(device, ps_descs[i].data, ps_descs[i].len, NULL, &ps_shaders[i]); + if (hr) Logger_Abort2(hr, "Failed to compile pixel shader"); + } +} + +static int PS_CalcShaderIndex(void) { + int idx = gfx_format == VERTEX_FORMAT_COLOURED ? 0 : 1; + if (gfx_alphaTest) idx += 2; + + if (gfx_fogEnabled) { + // uncomment when it works + if (ps_fogMode == FOG_LINEAR) idx += 4; + if (ps_fogMode == FOG_EXP) idx += 8; + } + return idx; +} + +static void PS_UpdateShader(void) { + int idx = PS_CalcShaderIndex(); + ID3D11DeviceContext_PSSetShader(context, ps_shaders[idx], NULL, 0); +} + +static void PS_FreeShaders(void) { + for (int i = 0; i < Array_Elems(ps_shaders); i++) + { + ID3D11PixelShader_Release(ps_shaders[i]); + } +} + +static void PS_CreateSamplers(void) { + // https://docs.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11device-createsamplerstate + // https://gamedev.stackexchange.com/questions/18026/directx11-how-do-i-manage-and-update-multiple-shader-constant-buffers + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-buffers-constant-how-to + D3D11_SAMPLER_DESC desc = { 0 }; + + desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + desc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP; + desc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP; + desc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; + desc.MaxAnisotropy = 1; + desc.MaxLOD = D3D11_FLOAT32_MAX; + desc.ComparisonFunc = D3D11_COMPARISON_NEVER; + + desc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + HRESULT hr1 = ID3D11Device_CreateSamplerState(device, &desc, &ps_samplers[0]); + + desc.Filter = D3D11_FILTER_MIN_MAG_POINT_MIP_LINEAR; + HRESULT hr2 = ID3D11Device_CreateSamplerState(device, &desc, &ps_samplers[1]); +} + +static void PS_UpdateSampler(void) { + ID3D11DeviceContext_PSSetSamplers(context, 0, 1, &ps_samplers[ps_mipmaps]); +} + +static void PS_FreeSamplers(void) { + for (int i = 0; i < Array_Elems(ps_samplers); i++) + { + ID3D11SamplerState_Release(ps_samplers[i]); + } +} + +static void PS_CreateConstants(void) { + D3D11_BUFFER_DESC desc = { 0 }; // TODO see notes in VS_CreateConstants + desc.ByteWidth = sizeof(ps_constants); + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + + D3D11_SUBRESOURCE_DATA data; + data.pSysMem = &ps_constants; + data.SysMemPitch = 0; + data.SysMemSlicePitch = 0; + + HRESULT hr = ID3D11Device_CreateBuffer(device, &desc, &data, &ps_cBuffer); + ID3D11DeviceContext_PSSetConstantBuffers(context, 0, 1, &ps_cBuffer); +} + +static void PS_UpdateConstants(void) { + ps_constants.fogR = PackedCol_R(ps_fogColor) / 255.0f; + ps_constants.fogG = PackedCol_G(ps_fogColor) / 255.0f; + ps_constants.fogB = PackedCol_B(ps_fogColor) / 255.0f; + + // avoid doing - in pixel shader for density fog + ps_constants.fogValue = ps_fogMode == FOG_LINEAR ? ps_fogEnd : -ps_fogDensity; + ID3D11DeviceContext_UpdateSubresource(context, ps_cBuffer, 0, NULL, &ps_constants, 0, 0); +} + +static void PS_FreeConstants(void) { + ID3D11Buffer_Release(ps_cBuffer); +} + +static void PS_Init(void) { + PS_CreateShaders(); + PS_CreateSamplers(); + PS_CreateConstants(); + PS_UpdateSampler(); + PS_UpdateShader(); +} + +static void PS_Free(void) { + PS_FreeShaders(); + PS_FreeSamplers(); + PS_FreeConstants(); +} + +static void SetAlphaTest(cc_bool enabled) { + PS_UpdateShader(); +} +// unnecessary? check if any performance is gained, probably irrelevant +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_BindTexture(GfxResourceID texId) { + /* defasult texture is otherwise transparent black */ + if (!texId) texId = white_square; + + ID3D11ShaderResourceView* view = (ID3D11ShaderResourceView*)texId; + ID3D11DeviceContext_PSSetShaderResources(context, 0, 1, &view); +} + +void Gfx_SetFog(cc_bool enabled) { + if (gfx_fogEnabled == enabled) return; + gfx_fogEnabled = enabled; + PS_UpdateShader(); +} + +void Gfx_SetFogCol(PackedCol color) { + if (color == ps_fogColor) return; + ps_fogColor = color; + PS_UpdateConstants(); +} + +void Gfx_SetFogDensity(float value) { + if (value == ps_fogDensity) return; + ps_fogDensity = value; + PS_UpdateConstants(); +} + +void Gfx_SetFogEnd(float value) { + if (value == ps_fogEnd) return; + ps_fogEnd = value; + PS_UpdateConstants(); +} + +void Gfx_SetFogMode(FogFunc func) { + if (ps_fogMode == func) return; + ps_fogMode = func; + PS_UpdateShader(); +} + +void Gfx_EnableMipmaps(void) { + if (!Gfx.Mipmaps) return; + ps_mipmaps = true; + PS_UpdateSampler(); +} + +void Gfx_DisableMipmaps(void) { + if (!Gfx.Mipmaps) return; + ps_mipmaps = false; + PS_UpdateSampler(); +} + + +//######################################################################################################################## +//-------------------------------------------------------Output merger---------------------------------------------------- +//######################################################################################################################## +// https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-output-merger-stage +static ID3D11RenderTargetView* backbuffer; +static ID3D11Texture2D* depthbuffer; +static ID3D11DepthStencilView* depthbufferView; +static ID3D11BlendState* om_blendStates[16 * 2]; +static ID3D11DepthStencilState* om_depthStates[4]; +static float gfx_clearColor[4]; +static cc_bool gfx_channels[4] = { true, true, true, true }; +static cc_bool gfx_depthTest, gfx_depthWrite; + +static void OM_Clear(GfxBuffers buffers) { + if (buffers & GFX_BUFFER_COLOR) { + ID3D11DeviceContext_ClearRenderTargetView(context, backbuffer, gfx_clearColor); + } + if (buffers & GFX_BUFFER_DEPTH) { + ID3D11DeviceContext_ClearDepthStencilView(context, depthbufferView, D3D11_CLEAR_DEPTH, 0.0f, 0); + } +} + +static void OM_UpdateTarget(void) { + ID3D11DeviceContext_OMSetRenderTargets(context, 1, &backbuffer, depthbufferView); +} + +static void OM_InitTargets(void) { + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-depth-stencil + D3D11_TEXTURE2D_DESC desc; + ID3D11Texture2D* pBackBuffer; + HRESULT hr; + + hr = IDXGISwapChain_GetBuffer(swapchain, 0, &guid_ID3D11Texture2D, (void**)&pBackBuffer); + if (hr) Logger_Abort2(hr, "Failed to get swapchain backbuffer"); + + hr = ID3D11Device_CreateRenderTargetView(device, pBackBuffer, NULL, &backbuffer); + if (hr) Logger_Abort2(hr, "Failed to create render target"); + + ID3D11Texture2D_GetDesc(pBackBuffer, &desc); + desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + desc.BindFlags = D3D11_BIND_DEPTH_STENCIL; + + hr = ID3D11Device_CreateTexture2D(device, &desc, NULL, &depthbuffer); + if (hr) Logger_Abort2(hr, "Failed to create depthbuffer texture"); + + hr = ID3D11Device_CreateDepthStencilView(device, depthbuffer, NULL, &depthbufferView); + if (hr) Logger_Abort2(hr, "Failed to create depthbuffer view"); + + ID3D11Texture2D_Release(pBackBuffer); + OM_UpdateTarget(); +} + +static void OM_CreateDepthStates(void) { + D3D11_DEPTH_STENCIL_DESC desc = { 0 }; + HRESULT hr; + desc.DepthFunc = D3D11_COMPARISON_GREATER_EQUAL; + + for (int i = 0; i < Array_Elems(om_depthStates); i++) + { + desc.DepthEnable = (i & 1) != 0; + desc.DepthWriteMask = (i & 2) != 0; + + hr = ID3D11Device_CreateDepthStencilState(device, &desc, &om_depthStates[i]); + if (hr) Logger_Abort2(hr, "Failed to create depth state"); + } +} + +static void OM_UpdateDepthState(void) { + ID3D11DepthStencilState* depthState = om_depthStates[gfx_depthTest | (gfx_depthWrite << 1)]; + ID3D11DeviceContext_OMSetDepthStencilState(context, depthState, 0); +} + +static void OM_FreeDepthStates(void) { + for (int i = 0; i < Array_Elems(om_depthStates); i++) + { + ID3D11DepthStencilState_Release(om_depthStates[i]); + } +} + +static void OM_CreateBlendStates(void) { + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-blend-state + D3D11_BLEND_DESC desc = { 0 }; + HRESULT hr; + desc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + desc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + desc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_SRC_ALPHA; + desc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + desc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + + for (int i = 0; i < Array_Elems(om_blendStates); i++) + { + int mask = 0; + if (i & 0x01) mask |= D3D11_COLOR_WRITE_ENABLE_RED; + if (i & 0x02) mask |= D3D11_COLOR_WRITE_ENABLE_GREEN; + if (i & 0x04) mask |= D3D11_COLOR_WRITE_ENABLE_BLUE; + if (i & 0x08) mask |= D3D11_COLOR_WRITE_ENABLE_ALPHA; + + desc.RenderTarget[0].RenderTargetWriteMask = mask; + desc.RenderTarget[0].BlendEnable = (i & 0x10) != 0; + + hr = ID3D11Device_CreateBlendState(device, &desc, &om_blendStates[i]); + if (hr) Logger_Abort2(hr, "Failed to create blend state"); + } +} + +static void OM_UpdateBlendState(void) { + int idx = (gfx_channels[0]) | (gfx_channels[1] << 1) | (gfx_channels[2] << 2) | (gfx_channels[3] << 3) | (gfx_alphaBlend << 4); + ID3D11BlendState* blendState = om_blendStates[idx]; + ID3D11DeviceContext_OMSetBlendState(context, blendState, NULL, 0xffffffff); +} + +static void OM_FreeBlendStates(void) { + for (int i = 0; i < Array_Elems(om_blendStates); i++) + { + ID3D11BlendState_Release(om_blendStates[i]); + } +} + +static void OM_Init(void) { + OM_InitTargets(); + OM_CreateDepthStates(); + OM_UpdateDepthState(); + OM_CreateBlendStates(); + OM_UpdateBlendState(); +} + +static void OM_FreeTargets(void) { + ID3D11DeviceContext_OMSetRenderTargets(context, 0, NULL, NULL); + ID3D11RenderTargetView_Release(backbuffer); + ID3D11DepthStencilView_Release(depthbufferView); + ID3D11Texture2D_Release(depthbuffer); +} + +static void OM_Free(void) { + OM_FreeTargets(); + OM_FreeDepthStates(); + OM_FreeBlendStates(); +} + +void Gfx_ClearColor(PackedCol color) { + gfx_clearColor[0] = PackedCol_R(color) / 255.0f; + gfx_clearColor[1] = PackedCol_G(color) / 255.0f; + gfx_clearColor[2] = PackedCol_B(color) / 255.0f; + gfx_clearColor[3] = PackedCol_A(color) / 255.0f; +} + +void Gfx_SetDepthTest(cc_bool enabled) { + gfx_depthTest = enabled; + OM_UpdateDepthState(); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + gfx_depthWrite = enabled; + OM_UpdateDepthState(); +} + +static void SetAlphaBlend(cc_bool enabled) { + OM_UpdateBlendState(); +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + gfx_channels[0] = r; + gfx_channels[1] = g; + gfx_channels[2] = b; + gfx_channels[3] = a; + OM_UpdateBlendState(); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +static BitmapCol* D3D11_GetRow(struct Bitmap* bmp, int y, void* ctx) { + D3D11_MAPPED_SUBRESOURCE* buffer = (D3D11_MAPPED_SUBRESOURCE*)ctx; + char* row = (char*)buffer->pData + y * buffer->RowPitch; + return (BitmapCol*)row; +} + +cc_result Gfx_TakeScreenshot(struct Stream* output) { + ID3D11Texture2D* tmp = NULL; + struct Bitmap bmp; + HRESULT hr; + + ID3D11Resource* backbuffer_res; + D3D11_RENDER_TARGET_VIEW_DESC backbuffer_desc; + D3D11_MAPPED_SUBRESOURCE buffer; + ID3D11RenderTargetView_GetResource(backbuffer, &backbuffer_res); + ID3D11RenderTargetView_GetDesc(backbuffer, &backbuffer_desc); + + D3D11_TEXTURE2D_DESC desc = { 0 }; + desc.Width = Window_Main.Width; + desc.Height = Window_Main.Height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_STAGING; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + hr = ID3D11Device_CreateTexture2D(device, &desc, NULL, &tmp); + if (hr) goto finished; + ID3D11DeviceContext_CopyResource(context, tmp, backbuffer_res); + + hr = ID3D11DeviceContext_Map(context, tmp, 0, D3D11_MAP_READ, 0, &buffer); + if (hr) goto finished; + { + Bitmap_Init(bmp, desc.Width, desc.Height, NULL); + hr = Png_Encode(&bmp, output, D3D11_GetRow, false, &buffer); + } + ID3D11DeviceContext_Unmap(context, tmp, 0); + +finished: + if (tmp) { ID3D11Texture2D_Release(tmp); } + ID3D11Resource_Release(backbuffer_res); + return hr; +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} +void Gfx_BeginFrame(void) { OM_UpdateTarget(); } + +void Gfx_ClearBuffers(GfxBuffers buffers) { + OM_Clear(buffers); +} + +void Gfx_EndFrame(void) { + // https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present + // gfx_vsync happens to match SyncInterval parameter + HRESULT hr = IDXGISwapChain_Present(swapchain, gfx_vsync, 0); + + // run at reduced FPS when minimised + if (hr == DXGI_STATUS_OCCLUDED) { + TickReducedPerformance(); return; + } + EndReducedPerformance(); + + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { + Gfx_LoseContext(" (Direct3D11 device lost)"); + } else if (hr) { + Logger_Abort2(hr, "Failed to swap buffers"); + } + if (gfx_minFrameMs) LimitFPS(); +} + +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +void Gfx_GetApiInfo(cc_string* info) { + int pointerSize = sizeof(void*) * 8; + HRESULT hr; + String_Format1(info, "-- Using Direct3D11 (%i bit) --\n", &pointerSize); + + // TODO this overlaps with global declarations, switch to them at some point.. ? + // apparently using D3D11CreateDeviceAndSwapChain is bad, need to investigate + IDXGIDevice* dxgi_device = NULL; + hr = ID3D11Device_QueryInterface(device, &guid_IXDGIDevice, &dxgi_device); + if (hr || !dxgi_device) return; + + IDXGIAdapter* dxgi_adapter; + hr = IDXGIDevice_GetAdapter(dxgi_device, &dxgi_adapter); + if (hr || !dxgi_adapter) goto release_device; + + DXGI_ADAPTER_DESC desc = { 0 }; + hr = IDXGIAdapter_GetDesc(dxgi_adapter, &desc); + if (hr) goto release_adapter; + + // desc.Description is a WCHAR, convert to char + char adapter[128] = { 0 }; + for (int i = 0; i < 128; i++) { adapter[i] = desc.Description[i]; } + + SIZE_T vram = desc.DedicatedVideoMemory; + SIZE_T dram = desc.DedicatedSystemMemory; + SIZE_T sram = desc.SharedSystemMemory; + float tram_ = (vram + dram + sram) / (1024.0 * 1024.0); + float vram_ = vram / (1024.0 * 1024.0); + + String_Format1(info, "Adapter: %c\n", adapter); + String_Format2(info, "Graphics memory: %f2 MB total (%f2 MB VRAM)\n", &tram_, &vram_); + PrintMaxTextureInfo(info); + +release_adapter: + IDXGIAdapter_Release(dxgi_adapter); +release_device: + IDXGIDevice_Release(dxgi_device); +} + +void Gfx_OnWindowResize(void) { + // https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi#handling-window-resizing + OM_FreeTargets(); + HRESULT hr = IDXGISwapChain_ResizeBuffers(swapchain, 0, 0, 0, DXGI_FORMAT_UNKNOWN, 0); + if (hr) Logger_Abort2(hr, "Failed to resize swapchain"); + + OM_InitTargets(); + Gfx_SetViewport(0, 0, Game.Width, Game.Height); +} + +static void InitPipeline(void) { + // https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline + IA_Init(); + VS_Init(); + RS_Init(); + PS_Init(); + OM_Init(); +} + +static void FreePipeline(void) { + IA_Free(); + VS_Free(); + RS_Free(); + PS_Free(); + OM_Free(); + + ID3D11DeviceContext_ClearState(context); + // Direct3D11 uses deferred resource destruction, so Flush to force destruction + // https://stackoverflow.com/questions/44155133/directx11-com-object-with-0-references-not-released + // https://stackoverflow.com/questions/20032816/can-someone-explain-why-i-still-have-live-objects-after-releasing-the-pointers-t + // https://www.gamedev.net/forums/topic/659651-dxgi-leak-warnings/5172345/ + ID3D11DeviceContext_Flush(context); +} +#endif diff --git a/src/Graphics_D3D9.c b/src/Graphics_D3D9.c new file mode 100644 index 0000000..fbec5b7 --- /dev/null +++ b/src/Graphics_D3D9.c @@ -0,0 +1,885 @@ +#include "Core.h" +#if CC_GFX_BACKEND == CC_GFX_BACKEND_D3D9 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" + +/* Avoid pointless includes */ +#define WIN32_LEAN_AND_MEAN +#define NOSERVICE +#define NOMCX +#define NOIME +/* NOTE: Direct3D9Ex backend was dropped on 2022-02-25 in favour of Direct3D11 */ +#include +#include +#include + +/* https://docs.microsoft.com/en-us/windows/win32/dxtecharts/resource-management-best-practices */ +/* https://docs.microsoft.com/en-us/windows/win32/dxtecharts/the-direct3d-transformation-pipeline */ + +/* https://docs.microsoft.com/en-us/windows/win32/direct3d9/d3dfvf-texcoordsizen */ +static DWORD d3d9_formatMappings[] = { D3DFVF_XYZ | D3DFVF_DIFFUSE, D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1 }; + +static IDirect3D9* d3d; +static IDirect3DDevice9* device; +static DWORD createFlags; +static D3DFORMAT viewFormat, depthFormat; +static int cachedWidth, cachedHeight; +static int depthBits; +static float totalMem; +static cc_bool fallbackRendering; + +static void D3D9_RestoreRenderStates(void); +static void D3D9_FreeResource(GfxResourceID resource) { + cc_uintptr addr; + ULONG refCount; + IUnknown* unk; + + unk = (IUnknown*)resource; + if (!unk) return; + +#ifdef __cplusplus + refCount = unk->Release(); +#else + refCount = unk->lpVtbl->Release(unk); +#endif + + if (refCount <= 0) return; + addr = (cc_uintptr)unk; + Platform_Log2("D3D9 resource has %i outstanding references! ID 0x%x", &refCount, &addr); +} + +static IDirect3D9* (WINAPI *_Direct3DCreate9)(UINT SDKVersion); + +static void LoadD3D9Library(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(Direct3DCreate9) + }; + static const cc_string path = String_FromConst("d3d9.dll"); + void* lib; + DynamicLib_LoadAll(&path, funcs, Array_Elems(funcs), &lib); + + if (lib) return; + Logger_FailToStart("Failed to load d3d9.dll. You may need to install Direct3D9."); +} + +static void CreateD3D9Instance(void) { + d3d = _Direct3DCreate9(D3D_SDK_VERSION); + + /* Normal Direct3D9 supports POOL_MANAGED textures */ + /* (Direct3D9Ex does not support them however) */ + Gfx.ManagedTextures = true; + if (!d3d) Logger_Abort("Direct3DCreate9 returned NULL"); + + fallbackRendering = Options_GetBool("fallback-rendering", false); + if (!fallbackRendering) return; + Platform_LogConst("WARNING: Using fallback rendering mode, which will reduce performance"); +} + +static void FindCompatibleViewFormat(void) { + static const D3DFORMAT formats[] = { D3DFMT_X8R8G8B8, D3DFMT_R8G8B8, D3DFMT_R5G6B5, D3DFMT_X1R5G5B5 }; + cc_result res; + int i; + + for (i = 0; i < Array_Elems(formats); i++) { + viewFormat = formats[i]; + res = IDirect3D9_CheckDeviceType(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, viewFormat, viewFormat, true); + if (!res) return; + } + Logger_FailToStart("Failed to create back buffer. Graphics drivers may not be installed.\n\nIf that still does not work, try the OpenGL build instead"); +} + +static void FindCompatibleDepthFormat(void) { + static const D3DFORMAT formats[] = { D3DFMT_D32, D3DFMT_D24X8, D3DFMT_D24S8, D3DFMT_D24X4S4, D3DFMT_D16, D3DFMT_D15S1 }; + cc_result res; + int i; + + for (i = 0; i < Array_Elems(formats); i++) { + depthFormat = formats[i]; + res = IDirect3D9_CheckDepthStencilMatch(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, viewFormat, viewFormat, depthFormat); + if (!res) return; + } + Logger_FailToStart("Failed to create depth buffer. Graphics drivers may not be installed.\n\nIf that still does not work, try the OpenGL build instead"); +} + +static void D3D9_FillPresentArgs(D3DPRESENT_PARAMETERS* args) { + args->AutoDepthStencilFormat = depthFormat; + args->BackBufferWidth = Game.Width; + args->BackBufferHeight = Game.Height; + args->BackBufferFormat = viewFormat; + args->BackBufferCount = 1; + args->EnableAutoDepthStencil = true; + args->PresentationInterval = gfx_vsync ? D3DPRESENT_INTERVAL_ONE : D3DPRESENT_INTERVAL_IMMEDIATE; + args->SwapEffect = D3DSWAPEFFECT_DISCARD; + args->Windowed = true; + //args->MultiSampleType = D3DMULTISAMPLE_8_SAMPLES; +} + +static const int D3D9_DepthBufferBits(void) { + switch (depthFormat) { + case D3DFMT_D32: return 32; + case D3DFMT_D24X8: return 24; + case D3DFMT_D24S8: return 24; + case D3DFMT_D24X4S4: return 24; + case D3DFMT_D16: return 16; + case D3DFMT_D15S1: return 15; + } + return 0; +} + +static void D3D9_UpdateCachedDimensions(void) { + cachedWidth = Game.Width; + cachedHeight = Game.Height; +} + +static cc_bool deviceCreated; +static void TryCreateDevice(void) { + cc_result res; + D3DCAPS9 caps; + HWND winHandle = (HWND)Window_Main.Handle; + D3DPRESENT_PARAMETERS args = { 0 }; + D3D9_FillPresentArgs(&args); + + /* Try to create a device with as much hardware usage as possible. */ + createFlags = D3DCREATE_HARDWARE_VERTEXPROCESSING; + res = IDirect3D9_CreateDevice(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, winHandle, createFlags, &args, &device); + /* Another running fullscreen application might prevent creating device */ + if (res == D3DERR_DEVICELOST) { Gfx.LostContext = true; return; } + + /* Fallback with using CPU for some parts of rendering */ + if (res) { + createFlags = D3DCREATE_MIXED_VERTEXPROCESSING; + res = IDirect3D9_CreateDevice(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, winHandle, createFlags, &args, &device); + } + if (res) { + createFlags = D3DCREATE_SOFTWARE_VERTEXPROCESSING; + res = IDirect3D9_CreateDevice(d3d, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, winHandle, createFlags, &args, &device); + } + + /* Not enough memory? Try again later in a bit */ + if (res == D3DERR_OUTOFVIDEOMEMORY) { Gfx.LostContext = true; return; } + + if (res) Logger_Abort2(res, "Creating Direct3D9 device"); + res = IDirect3DDevice9_GetDeviceCaps(device, &caps); + if (res) Logger_Abort2(res, "Getting Direct3D9 capabilities"); + + D3D9_UpdateCachedDimensions(); + deviceCreated = true; + Gfx.MaxTexWidth = caps.MaxTextureWidth; + Gfx.MaxTexHeight = caps.MaxTextureHeight; + totalMem = IDirect3DDevice9_GetAvailableTextureMem(device) / (1024.0f * 1024.0f); +} + +void Gfx_Create(void) { + LoadD3D9Library(); + CreateD3D9Instance(); + FindCompatibleViewFormat(); + FindCompatibleDepthFormat(); + depthBits = D3D9_DepthBufferBits(); + + customMipmapsLevels = true; + Gfx.Created = true; + TryCreateDevice(); +} + +cc_bool Gfx_TryRestoreContext(void) { + static int availFails; + D3DPRESENT_PARAMETERS args = { 0 }; + cc_result res; + + /* Rarely can't even create device to begin with */ + if (!deviceCreated) { + TryCreateDevice(); + return deviceCreated; + } + + res = IDirect3DDevice9_TestCooperativeLevel(device); + if (res && res != D3DERR_DEVICENOTRESET) return false; + + D3D9_FillPresentArgs(&args); + res = IDirect3DDevice9_Reset(device, &args); + if (res == D3DERR_DEVICELOST) return false; + + /* A user reported an issue where after changing some settings in */ + /* nvidia control panel, IDirect3DDevice9_Reset would return */ + /* D3DERR_NOTAVAILABLE and hence crash the game */ + /* So try to workaround this by only crashing after 50 failures */ + if (res == D3DERR_NOTAVAILABLE && availFails++ < 50) return false; + + if (res) Logger_Abort2(res, "Error recreating D3D9 context"); + D3D9_UpdateCachedDimensions(); + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); + D3D9_FreeResource(device); device = NULL; + D3D9_FreeResource(d3d); d3d = NULL; +} + +static void Gfx_FreeState(void) { + FreeDefaultResources(); + cachedWidth = 0; + cachedHeight = 0; +} + +static void Gfx_RestoreState(void) { + Gfx_SetFaceCulling(false); + InitDefaultResources(); + gfx_format = -1; + + IDirect3DDevice9_SetRenderState(device, D3DRS_COLORVERTEX, false); + IDirect3DDevice9_SetRenderState(device, D3DRS_LIGHTING, false); + IDirect3DDevice9_SetRenderState(device, D3DRS_SPECULARENABLE, false); + IDirect3DDevice9_SetRenderState(device, D3DRS_LOCALVIEWER, false); + IDirect3DDevice9_SetRenderState(device, D3DRS_DEBUGMONITORTOKEN, false); + + /* States relevant to the game */ + IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHAFUNC, D3DCMP_GREATER); + IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHAREF, 127); + IDirect3DDevice9_SetRenderState(device, D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + IDirect3DDevice9_SetRenderState(device, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + IDirect3DDevice9_SetRenderState(device, D3DRS_ZFUNC, D3DCMP_GREATEREQUAL); + D3D9_RestoreRenderStates(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +static void D3D9_SetTextureData(IDirect3DTexture9* texture, struct Bitmap* bmp, int rowWidth, int lvl) { + D3DLOCKED_RECT rect; + cc_result res = IDirect3DTexture9_LockRect(texture, lvl, &rect, NULL, 0); + if (res) Logger_Abort2(res, "D3D9_LockTextureData"); + + CopyTextureData(rect.pBits, rect.Pitch, bmp, rowWidth << 2); + + res = IDirect3DTexture9_UnlockRect(texture, lvl); + if (res) Logger_Abort2(res, "D3D9_UnlockTextureData"); +} + +static void D3D9_SetTexturePartData(IDirect3DTexture9* texture, int x, int y, const struct Bitmap* bmp, int rowWidth, int lvl) { + D3DLOCKED_RECT rect; + cc_result res; + RECT part; + part.left = x; part.right = x + bmp->width; + part.top = y; part.bottom = y + bmp->height; + + res = IDirect3DTexture9_LockRect(texture, lvl, &rect, &part, 0); + if (res) Logger_Abort2(res, "D3D9_LockTexturePartData"); + + CopyTextureData(rect.pBits, rect.Pitch, bmp, rowWidth << 2); + + res = IDirect3DTexture9_UnlockRect(texture, lvl); + if (res) Logger_Abort2(res, "D3D9_UnlockTexturePartData"); +} + +static void D3D9_DoMipmaps(IDirect3DTexture9* texture, int x, int y, struct Bitmap* bmp, int rowWidth, cc_bool partial) { + BitmapCol* prev = bmp->scan0; + BitmapCol* cur; + struct Bitmap mipmap; + + int lvls = CalcMipmapsLevels(bmp->width, bmp->height); + int lvl, width = bmp->width, height = bmp->height; + + for (lvl = 1; lvl <= lvls; lvl++) { + x /= 2; y /= 2; + if (width > 1) width /= 2; + if (height > 1) height /= 2; + + cur = (BitmapCol*)Mem_Alloc(width * height, 4, "mipmaps"); + GenMipmaps(width, height, cur, prev, rowWidth); + + Bitmap_Init(mipmap, width, height, cur); + if (partial) { + D3D9_SetTexturePartData(texture, x, y, &mipmap, width, lvl); + } else { + D3D9_SetTextureData(texture, &mipmap, width, lvl); + } + + if (prev != bmp->scan0) Mem_Free(prev); + prev = cur; + rowWidth = width; + } + if (prev != bmp->scan0) Mem_Free(prev); +} + +static cc_bool D3D9_CheckResult(cc_result res, const char* func) { + if (!res) return true; + + if (res == D3DERR_OUTOFVIDEOMEMORY || res == E_OUTOFMEMORY) { + if (!Game_ReduceVRAM()) Logger_Abort("Out of video memory!"); + } else { + Logger_Abort2(res, func); + } + return false; +} + +static IDirect3DTexture9* DoCreateTexture(struct Bitmap* bmp, int levels, int pool) { + IDirect3DTexture9* tex; + cc_result res; + + for (;;) { + res = IDirect3DDevice9_CreateTexture(device, bmp->width, bmp->height, levels, + 0, D3DFMT_A8R8G8B8, pool, &tex, NULL); + if (D3D9_CheckResult(res, "D3D9_CreateTexture failed")) break; + } + return tex; +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + IDirect3DTexture9* tex; + IDirect3DTexture9* sys; + cc_result res; + + int mipmapsLevels = CalcMipmapsLevels(bmp->width, bmp->height); + int levels = 1 + (mipmaps ? mipmapsLevels : 0); + + if (flags & TEXTURE_FLAG_MANAGED) { + while ((res = IDirect3DDevice9_CreateTexture(device, bmp->width, bmp->height, levels, + 0, D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, &tex, NULL))) + { + if (res == D3DERR_OUTOFVIDEOMEMORY || res == E_OUTOFMEMORY) { + /* insufficient VRAM or RAM left to allocate texture, try to reduce the memory in use first */ + /* if can't reduce, return 'empty' texture so that at least the game will continue running */ + if (!Game_ReduceVRAM()) return 0; + } else { + /* unknown issue, so don't even try to handle the error */ + Logger_Abort2(res, "D3D9_CreateManagedTexture failed"); + } + } + + D3D9_SetTextureData(tex, bmp, rowWidth, 0); + if (mipmaps) D3D9_DoMipmaps(tex, 0, 0, bmp, rowWidth, false); + return tex; + } + + sys = DoCreateTexture(bmp, levels, D3DPOOL_SYSTEMMEM); + D3D9_SetTextureData(sys, bmp, rowWidth, 0); + if (mipmaps) D3D9_DoMipmaps(sys, 0, 0, bmp, rowWidth, false); + + tex = DoCreateTexture(bmp, levels, D3DPOOL_DEFAULT); + res = IDirect3DDevice9_UpdateTexture(device, (IDirect3DBaseTexture9*)sys, (IDirect3DBaseTexture9*)tex); + if (res) Logger_Abort2(res, "D3D9_CreateTexture - Update"); + + D3D9_FreeResource(sys); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + IDirect3DTexture9* texture = (IDirect3DTexture9*)texId; + D3D9_SetTexturePartData(texture, x, y, part, rowWidth, 0); + if (mipmaps) D3D9_DoMipmaps(texture, x, y, part, rowWidth, true); +} + +void Gfx_BindTexture(GfxResourceID texId) { + cc_result res = IDirect3DDevice9_SetTexture(device, 0, (IDirect3DBaseTexture9*)texId); + if (res) Logger_Abort2(res, "D3D9_BindTexture"); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { D3D9_FreeResource(*texId); *texId = NULL; } + +void Gfx_EnableMipmaps(void) { + if (!Gfx.Mipmaps) return; + IDirect3DDevice9_SetSamplerState(device, 0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR); +} + +void Gfx_DisableMipmaps(void) { + if (!Gfx.Mipmaps) return; + IDirect3DDevice9_SetSamplerState(device, 0, D3DSAMP_MIPFILTER, D3DTEXF_NONE); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static D3DFOGMODE gfx_fogMode = D3DFOG_NONE; +static cc_bool gfx_alphaBlending; +static cc_bool gfx_depthTesting, gfx_depthWriting; +static PackedCol gfx_clearColor, gfx_fogColor; +static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f; + +/* NOTE: Although SetRenderState is okay to call on a lost device, it's also possible */ +/* the context is lost because the device was never created to begin with! */ +/* In that case, device will be NULL, so calling SetRenderState will crash the game. */ +/* (see Gfx_Create, TryCreateDevice, Gfx_TryRestoreContext) */ + +void Gfx_SetFaceCulling(cc_bool enabled) { + D3DCULL mode = enabled ? D3DCULL_CW : D3DCULL_NONE; + IDirect3DDevice9_SetRenderState(device, D3DRS_CULLMODE, mode); +} + +void Gfx_SetFog(cc_bool enabled) { + if (gfx_fogEnabled == enabled) return; + gfx_fogEnabled = enabled; + + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGENABLE, enabled); +} + +void Gfx_SetFogCol(PackedCol color) { + if (color == gfx_fogColor) return; + gfx_fogColor = color; + + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGCOLOR, gfx_fogColor); +} + +void Gfx_SetFogDensity(float value) { + union IntAndFloat raw; + if (value == gfx_fogDensity) return; + gfx_fogDensity = value; + + raw.f = value; + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGDENSITY, raw.u); +} + +void Gfx_SetFogEnd(float value) { + union IntAndFloat raw; + if (value == gfx_fogEnd) return; + gfx_fogEnd = value; + + raw.f = value; + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGEND, raw.u); +} + +void Gfx_SetFogMode(FogFunc func) { + static D3DFOGMODE modes[3] = { D3DFOG_LINEAR, D3DFOG_EXP, D3DFOG_EXP2 }; + D3DFOGMODE mode = modes[func]; + if (mode == gfx_fogMode) return; + + gfx_fogMode = mode; + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGTABLEMODE, mode); +} + +static void SetAlphaTest(cc_bool enabled) { + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHATESTENABLE, enabled); +} + +static void SetAlphaBlend(cc_bool enabled) { + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHABLENDENABLE, enabled); +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { + D3DTEXTUREOP op = enabled ? D3DTOP_MODULATE : D3DTOP_SELECTARG1; + if (Gfx.LostContext) return; + IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_ALPHAOP, op); +} + +void Gfx_ClearColor(PackedCol color) { gfx_clearColor = color; } + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + DWORD channels = (r ? 1u : 0u) | (g ? 2u : 0u) | (b ? 4u : 0u) | (a ? 8u : 0u); + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_COLORWRITEENABLE, channels); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + gfx_depthTesting = enabled; + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_ZENABLE, enabled); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + gfx_depthWriting = enabled; + if (Gfx.LostContext) return; + IDirect3DDevice9_SetRenderState(device, D3DRS_ZWRITEENABLE, enabled); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); + if (depthOnly) IDirect3DDevice9_SetTexture(device, 0, NULL); + + /* For when Direct3D9 device doesn't support D3DRS_COLORWRITEENABLE */ + /* Technically, the correct way to check for whether it works or not */ + /* is by checking whether the PrimitiveMiscCaps field in D3DCAPS9 */ + /* has the D3DPMISCCAPS_COLORWRITEENABLE flag set */ + /* But since I'm unsure if there might be some GPU drivers out there */ + /* that do support D3DRS_COLORWRITEENABLE but forget to set the flag, */ + /* I've decided to require the user to manually enable this fallback */ + if (!fallbackRendering) return; + + /* https://gamedev.net/forums/topic/375017-c-enabling-disabling-writing-to-buffers/3473819/ */ + if (depthOnly) { + /* finalX = srcX*0 + dstX*1 */ + /* So in other words, final pixel = existing pixel */ + /* Pretty costly performance wise though */ + IDirect3DDevice9_SetRenderState(device, D3DRS_SRCBLEND, D3DBLEND_ZERO); + IDirect3DDevice9_SetRenderState(device, D3DRS_DESTBLEND, D3DBLEND_ONE); + Gfx_SetAlphaBlending(true); + } else { + /* finalX = srcX*srcA + dstX*(1-srcA) */ + IDirect3DDevice9_SetRenderState(device, D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + IDirect3DDevice9_SetRenderState(device, D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + } +} + +static void D3D9_RestoreRenderStates(void) { + union IntAndFloat raw; + IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHATESTENABLE, gfx_alphaTest); + IDirect3DDevice9_SetRenderState(device, D3DRS_ALPHABLENDENABLE, gfx_alphaBlending); + + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGENABLE, gfx_fogEnabled); + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGCOLOR, gfx_fogColor); + raw.f = gfx_fogDensity; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGDENSITY, raw.u); + raw.f = gfx_fogEnd; + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGEND, raw.u); + IDirect3DDevice9_SetRenderState(device, D3DRS_FOGTABLEMODE, gfx_fogMode); + + IDirect3DDevice9_SetRenderState(device, D3DRS_ZENABLE, gfx_depthTesting); + IDirect3DDevice9_SetRenderState(device, D3DRS_ZWRITEENABLE, gfx_depthWriting); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static void D3D9_SetIbData(IDirect3DIndexBuffer9* buffer, int count, Gfx_FillIBFunc fillFunc, void* obj) { + void* dst = NULL; + cc_result res = IDirect3DIndexBuffer9_Lock(buffer, 0, count * 2, &dst, 0); + if (res) Logger_Abort2(res, "D3D9_LockIb"); + + fillFunc((cc_uint16*)dst, count, obj); + res = IDirect3DIndexBuffer9_Unlock(buffer); + if (res) Logger_Abort2(res, "D3D9_UnlockIb"); +} + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + int size = count * 2; + IDirect3DIndexBuffer9* ibuffer; + cc_result res = IDirect3DDevice9_CreateIndexBuffer(device, size, D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_DEFAULT, &ibuffer, NULL); + if (res) Logger_Abort2(res, "D3D9_CreateIb"); + + D3D9_SetIbData(ibuffer, count, fillFunc, obj); + return ibuffer; +} + +void Gfx_BindIb(GfxResourceID ib) { + IDirect3DIndexBuffer9* ibuffer = (IDirect3DIndexBuffer9*)ib; + cc_result res = IDirect3DDevice9_SetIndices(device, ibuffer); + if (res) Logger_Abort2(res, "D3D9_BindIb"); +} + +void Gfx_DeleteIb(GfxResourceID* ib) { D3D9_FreeResource(*ib); *ib = NULL; } + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static IDirect3DVertexBuffer9* D3D9_AllocVertexBuffer(VertexFormat fmt, int count, DWORD usage) { + IDirect3DVertexBuffer9* vbuffer; + int size = count * strideSizes[fmt]; + cc_result res; + + res = IDirect3DDevice9_CreateVertexBuffer(device, size, usage, + d3d9_formatMappings[fmt], D3DPOOL_DEFAULT, &vbuffer, NULL); + + if (res == D3DERR_OUTOFVIDEOMEMORY || res == E_OUTOFMEMORY) + return NULL; + + if (res) Logger_Abort2(res, "D3D9_AllocVertexBuffer failed"); + return vbuffer; +} + +static void D3D9_SetVbData(IDirect3DVertexBuffer9* buffer, void* data, int size, int lockFlags) { + void* dst = NULL; + cc_result res = IDirect3DVertexBuffer9_Lock(buffer, 0, size, &dst, lockFlags); + if (res) Logger_Abort2(res, "D3D9_LockVb"); + + Mem_Copy(dst, data, size); + res = IDirect3DVertexBuffer9_Unlock(buffer); + if (res) Logger_Abort2(res, "D3D9_UnlockVb"); +} + +static void* D3D9_LockVb(GfxResourceID vb, VertexFormat fmt, int count, int lockFlags) { + IDirect3DVertexBuffer9* buffer = (IDirect3DVertexBuffer9*)vb; + void* dst = NULL; + int size = count * strideSizes[fmt]; + + cc_result res = IDirect3DVertexBuffer9_Lock(buffer, 0, size, &dst, lockFlags); + if (res) Logger_Abort2(res, "D3D9_LockVb"); + return dst; +} + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return D3D9_AllocVertexBuffer(fmt, count, D3DUSAGE_WRITEONLY); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { D3D9_FreeResource(*vb); *vb = NULL; } + +void Gfx_BindVb(GfxResourceID vb) { + IDirect3DVertexBuffer9* vbuffer = (IDirect3DVertexBuffer9*)vb; + cc_result res = IDirect3DDevice9_SetStreamSource(device, 0, vbuffer, 0, gfx_stride); + if (res) Logger_Abort2(res, "D3D9_BindVb"); +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return D3D9_LockVb(vb, fmt, count, 0); +} + +void Gfx_UnlockVb(GfxResourceID vb) { + IDirect3DVertexBuffer9* buffer = (IDirect3DVertexBuffer9*)vb; + cc_result res = IDirect3DVertexBuffer9_Unlock(buffer); + if (res) Logger_Abort2(res, "Gfx_UnlockVb"); +} + + +/*########################################################################################################################* +*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return D3D9_AllocVertexBuffer(fmt, maxVertices, D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { D3D9_FreeResource(*vb); *vb = NULL; } + +void Gfx_BindDynamicVb(GfxResourceID vb) { + IDirect3DVertexBuffer9* vbuffer = (IDirect3DVertexBuffer9*)vb; + cc_result res = IDirect3DDevice9_SetStreamSource(device, 0, vbuffer, 0, gfx_stride); + if (res) Logger_Abort2(res, "D3D9_BindDynamicVb"); +} + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return D3D9_LockVb(vb, fmt, count, D3DLOCK_DISCARD); +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + Gfx_UnlockVb(vb); + Gfx_BindDynamicVb(vb); /* TODO: Inline this? */ +} + +void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount) { + int size = vCount * gfx_stride; + IDirect3DVertexBuffer9* buffer = (IDirect3DVertexBuffer9*)vb; + D3D9_SetVbData(buffer, vertices, size, D3DLOCK_DISCARD); + + cc_result res = IDirect3DDevice9_SetStreamSource(device, 0, buffer, 0, gfx_stride); + if (res) Logger_Abort2(res, "D3D9_SetDynamicVbData - Bind"); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Vertex rendering----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + cc_result res; + if (fmt == gfx_format) return; + gfx_format = fmt; + + if (fmt == VERTEX_FORMAT_COLOURED) { + /* it's necessary to unbind the texture, otherwise the alpha from the last bound texture */ + /* gets used - because D3DTSS_ALPHAOP texture stage state is still set to D3DTOP_SELECTARG1 */ + IDirect3DDevice9_SetTexture(device, 0, NULL); + /* IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_COLOROP, fmt == VERTEX_FORMAT_COLOURED ? D3DTOP_DISABLE : D3DTOP_MODULATE); */ + /* IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_ALPHAOP, fmt == VERTEX_FORMAT_COLOURED ? D3DTOP_DISABLE : D3DTOP_SELECTARG1); */ + /* SetTexture(NULL) seems to be enough, not really required to call SetTextureStageState */ + } + + res = IDirect3DDevice9_SetFVF(device, d3d9_formatMappings[fmt]); + if (res) Logger_Abort2(res, "D3D9_SetVertexFormat"); + gfx_stride = strideSizes[fmt]; +} + +void Gfx_DrawVb_Lines(int verticesCount) { + /* NOTE: Skip checking return result for Gfx_DrawXYZ for performance */ + IDirect3DDevice9_DrawPrimitive(device, D3DPT_LINELIST, 0, verticesCount >> 1); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + IDirect3DDevice9_DrawIndexedPrimitive(device, D3DPT_TRIANGLELIST, + 0, 0, verticesCount, 0, verticesCount >> 1); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + IDirect3DDevice9_DrawIndexedPrimitive(device, D3DPT_TRIANGLELIST, + startVertex, 0, verticesCount, 0, verticesCount >> 1); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + IDirect3DDevice9_DrawIndexedPrimitive(device, D3DPT_TRIANGLELIST, + startVertex, 0, verticesCount, 0, verticesCount >> 1); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static D3DTRANSFORMSTATETYPE matrix_modes[2] = { D3DTS_PROJECTION, D3DTS_VIEW }; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (Gfx.LostContext) return; + IDirect3DDevice9_SetTransform(device, matrix_modes[type], (const D3DMATRIX*)matrix); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + if (Gfx.LostContext) return; + IDirect3DDevice9_SetTransform(device, matrix_modes[type], (const D3DMATRIX*)&Matrix_Identity); +} + +static struct Matrix texMatrix = Matrix_IdentityValue; +void Gfx_EnableTextureOffset(float x, float y) { + texMatrix.row3.x = x; texMatrix.row3.y = y; + if (Gfx.LostContext) return; + + IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2); + IDirect3DDevice9_SetTransform(device, D3DTS_TEXTURE0, (const D3DMATRIX*)&texMatrix); +} + +void Gfx_DisableTextureOffset(void) { + if (Gfx.LostContext) return; + IDirect3DDevice9_SetTextureStageState(device, 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE); + IDirect3DDevice9_SetTransform(device, D3DTS_TEXTURE0, (const D3DMATRIX*)&Matrix_Identity); +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + /* Deliberately swap zNear/zFar in projection matrix calculation to produce */ + /* a projection matrix that results in a reversed depth buffer */ + /* https://developer.nvidia.com/content/depth-precision-visualized */ + float zNear_ = zFar; + float zFar_ = Reversed_CalcZNear(fov, depthBits); + + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar_ / (zNear_ - zFar_); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear_ * zFar_) / (zNear_ - zFar_); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + IDirect3DSurface9* backbuffer = NULL; + IDirect3DSurface9* temp = NULL; + D3DSURFACE_DESC desc; + D3DLOCKED_RECT rect; + struct Bitmap bmp; + cc_result res; + + res = IDirect3DDevice9_GetBackBuffer(device, 0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer); + if (res) goto finished; + res = IDirect3DSurface9_GetDesc(backbuffer, &desc); + if (res) goto finished; + + res = IDirect3DDevice9_CreateOffscreenPlainSurface(device, desc.Width, desc.Height, D3DFMT_X8R8G8B8, D3DPOOL_SYSTEMMEM, &temp, NULL); + if (res) goto finished; /* TODO: For DX 8 use IDirect3DDevice8::CreateImageSurface */ + res = IDirect3DDevice9_GetRenderTargetData(device, backbuffer, temp); + if (res) goto finished; + + res = IDirect3DSurface9_LockRect(temp, &rect, NULL, D3DLOCK_READONLY | D3DLOCK_NO_DIRTY_UPDATE); + if (res) goto finished; + { + Bitmap_Init(bmp, desc.Width, desc.Height, (BitmapCol*)rect.pBits); + res = Png_Encode(&bmp, output, NULL, false, NULL); + if (res) { IDirect3DSurface9_UnlockRect(temp); goto finished; } + } + res = IDirect3DSurface9_UnlockRect(temp); + if (res) goto finished; + +finished: + D3D9_FreeResource(backbuffer); + D3D9_FreeResource(temp); + return res; +} + +static void UpdateSwapchain(const char* reason) { + /* TODO: Can Direct3D9Ex fast path still be used here? */ + Gfx_LoseContext(reason); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + if (gfx_vsync == vsync) return; + + gfx_vsync = vsync; + if (device) UpdateSwapchain(" (toggling VSync)"); +} + +void Gfx_BeginFrame(void) { + IDirect3DDevice9_BeginScene(device); +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + DWORD targets = 0; + if (buffers & GFX_BUFFER_COLOR) targets |= D3DCLEAR_TARGET; + if (buffers & GFX_BUFFER_DEPTH) targets |= D3DCLEAR_ZBUFFER; + + cc_result res = IDirect3DDevice9_Clear(device, 0, NULL, targets, gfx_clearColor, 0.0f, 0); + if (res) Logger_Abort2(res, "D3D9_Clear"); +} + +void Gfx_EndFrame(void) { + IDirect3DDevice9_EndScene(device); + cc_result res = IDirect3DDevice9_Present(device, NULL, NULL, NULL, NULL); + + if (res) { + if (res != D3DERR_DEVICELOST) Logger_Abort2(res, "D3D9_EndFrame"); + /* TODO: Make sure this actually works on all graphics cards. */ + Gfx_LoseContext(" (Direct3D9 device lost)"); + } + if (gfx_minFrameMs) LimitFPS(); +} + +cc_bool Gfx_WarnIfNecessary(void) { return false; } +static const char* D3D9_StrFlags(void) { + if (createFlags & D3DCREATE_HARDWARE_VERTEXPROCESSING) return "Hardware"; + if (createFlags & D3DCREATE_MIXED_VERTEXPROCESSING) return "Mixed"; + if (createFlags & D3DCREATE_SOFTWARE_VERTEXPROCESSING) return "Software"; + return "(none)"; +} + +void Gfx_GetApiInfo(cc_string* info) { + D3DADAPTER_IDENTIFIER9 adapter = { 0 }; + int pointerSize = sizeof(void*) * 8; + float curMem; + + IDirect3D9_GetAdapterIdentifier(d3d, D3DADAPTER_DEFAULT, 0, &adapter); + curMem = IDirect3DDevice9_GetAvailableTextureMem(device) / (1024.0f * 1024.0f); + String_Format1(info, "-- Using Direct3D9 (%i bit) --\n", &pointerSize); + + String_Format1(info, "Adapter: %c\n", adapter.Description); + String_Format1(info, "Processing mode: %c\n", D3D9_StrFlags()); + String_Format2(info, "Video memory: %f2 MB total, %f2 free\n", &totalMem, &curMem); + PrintMaxTextureInfo(info); + String_Format1(info, "Depth buffer bits: %i", &depthBits); +} + +void Gfx_OnWindowResize(void) { + if (Game.Width == cachedWidth && Game.Height == cachedHeight) return; + /* Only resize when necessary */ + UpdateSwapchain(" (resizing window)"); +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } +#endif diff --git a/src/Graphics_Dreamcast.c b/src/Graphics_Dreamcast.c new file mode 100644 index 0000000..8da6c8c --- /dev/null +++ b/src/Graphics_Dreamcast.c @@ -0,0 +1,643 @@ +#include "Core.h" +#if defined CC_BUILD_DREAMCAST +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include "../third_party/gldc/src/sh4.h" +#include +#include +#include +#include + +static cc_bool renderingDisabled; +#define VERTEX_BUFFER_SIZE 32 * 40000 + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static int InitPowerVR(void) { + cc_bool autosort = false; // Turn off auto sorting to match traditional GPU behaviour + cc_bool fsaa = false; + AUTOSORT_ENABLED = autosort; + + pvr_init_params_t params = { + // Opaque, punch through, translucent polygons with largest bin sizes + { PVR_BINSIZE_32, PVR_BINSIZE_0, PVR_BINSIZE_32, PVR_BINSIZE_0, PVR_BINSIZE_32 }, + VERTEX_BUFFER_SIZE, + 0, fsaa, + (autosort) ? 0 : 1 + }; + pvr_init(¶ms); +} + +static void InitGLState(void) { + glClearDepth(1.0f); + GPUSetAlphaCutOff(127); + + ALPHA_TEST_ENABLED = GL_FALSE; + CULLING_ENABLED = GL_FALSE; + BLEND_ENABLED = GL_FALSE; + DEPTH_TEST_ENABLED = GL_FALSE; + DEPTH_MASK_ENABLED = GL_TRUE; + TEXTURES_ENABLED = GL_FALSE; + FOG_ENABLED = GL_FALSE; + + STATE_DIRTY = GL_TRUE; +} + +void Gfx_Create(void) { + if (!Gfx.Created) InitPowerVR(); + if (!Gfx.Created) glKosInit(); + + Gfx_SetViewport(0, 0, Game.Width, Game.Height); + InitGLState(); + + Gfx.MinTexWidth = 8; + Gfx.MinTexHeight = 8; + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.MaxTexSize = 512 * 512; // reasonable cap as Dreamcast only has 8MB VRAM + Gfx.Created = true; + + Gfx_RestoreState(); +} + +cc_bool Gfx_TryRestoreContext(void) { + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_clearColor; + +void Gfx_SetFaceCulling(cc_bool enabled) { + CULLING_ENABLED = enabled; + STATE_DIRTY = GL_TRUE; +} + +static void SetAlphaBlend(cc_bool enabled) { + BLEND_ENABLED = enabled; + STATE_DIRTY = GL_TRUE; +} +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_ClearColor(PackedCol color) { + if (color == gfx_clearColor) return; + gfx_clearColor = color; + + float r = PackedCol_R(color) / 255.0f; + float g = PackedCol_G(color) / 255.0f; + float b = PackedCol_B(color) / 255.0f; + pvr_set_bg_color(r, g, b); // TODO: not working ? +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO: Doesn't work +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + if (DEPTH_MASK_ENABLED == enabled) return; + + DEPTH_MASK_ENABLED = enabled; + STATE_DIRTY = GL_TRUE; +} + +void Gfx_SetDepthTest(cc_bool enabled) { + if (DEPTH_TEST_ENABLED == enabled) return; + + DEPTH_TEST_ENABLED = enabled; + STATE_DIRTY = GL_TRUE; +} + +static void SetAlphaTest(cc_bool enabled) { + ALPHA_TEST_ENABLED = enabled; + STATE_DIRTY = GL_TRUE; +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + // don't need a fake second pass in this case + renderingDisabled = depthOnly; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -2.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -(zFar + zNear) / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum */ + /* For a FOV based perspective matrix, left/right/top/bottom are calculated as: */ + /* left = -c * aspect, right = c * aspect, bottom = -c, top = c */ + /* Calculations are simplified because of left/right and top/bottom symmetry */ + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static void* gfx_vertices; +static int vb_size; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return memalign(16, count * strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; + //sceKernelDcacheWritebackInvalidateRange(vb, vb_size); +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return memalign(16, maxVertices * strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; + //dcache_flush_range(vb, vb_size); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_BindTexture(GfxResourceID texId) { + gldcBindTexture((GLuint)texId); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GLuint id = (GLuint)(*texId); + if (!id) return; + gldcDeleteTexture(id); + *texId = 0; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +static unsigned Interleave(unsigned x) { + // Simplified "Interleave bits by Binary Magic Numbers" from + // http://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN + + x = (x | (x << 8)) & 0x00FF00FF; + x = (x | (x << 4)) & 0x0F0F0F0F; + x = (x | (x << 2)) & 0x33333333; + x = (x | (x << 1)) & 0x55555555; + return x; +} + +/*static int CalcTwiddledIndex(int x, int y, int w, int h) { + // Twiddled index looks like this (lowest numbered bits are leftmost): + // - w = h: yxyx yxyx + // - w > h: yxyx xxxx + // - h > w: yxyx yyyy + // And can therefore be broken down into two components: + // 1) interleaved lower bits + // 2) masked and then shifted higher bits + + int min_dimension = Math.Min(w, h); + + int interleave_mask = min_dimension - 1; + int interleaved_bits = Math_ilog2(min_dimension); + + int shifted_mask = (~0) & ~interleave_mask; + // as lower interleaved bits contain both X and Y, need to adjust the + // higher bit shift by number of interleaved bits used by other axis + int shift_bits = interleaved_bits; + + // For example, in the case of W=4 and H=8 + // the bit pattern is yx_yx_yx_Y + // - lower 3 Y bits are interleaved + // - upper 1 Y bit must be shifted right 3 bits + + int lo_Y = Interleave(y & interleave_mask); + int hi_Y = (y & shifted_mask) << shift_bits; + int Y = lo_Y | hi_Y; + + int lo_X = Interleave(x & interleave_mask) << 1; + int hi_X = (x & shifted_mask) << shift_bits; + int X = lo_X | hi_X; + + return X | Y; +}*/ + +#define Twiddle_CalcFactors(w, h) \ + min_dimension = min(w, h); \ + interleave_mask = min_dimension - 1; \ + interleaved_bits = Math_ilog2(min_dimension); \ + shifted_mask = ~interleave_mask; \ + shift_bits = interleaved_bits; + +#define Twiddle_CalcY(y) \ + lo_Y = Interleave(y & interleave_mask); \ + hi_Y = (y & shifted_mask) << shift_bits; \ + Y = lo_Y | hi_Y; + +#define Twiddle_CalcX(x) \ + lo_X = Interleave(x & interleave_mask) << 1; \ + hi_X = (x & shifted_mask) << shift_bits; \ + X = lo_X | hi_X; + + +// B8 G8 R8 A8 > B4 G4 R4 A4 +#define BGRA8_to_BGRA4(src) \ + ((src[0] & 0xF0) >> 4) | (src[1] & 0xF0) | ((src[2] & 0xF0) << 4) | ((src[3] & 0xF0) << 8); + +static void ConvertTexture(cc_uint16* dst, struct Bitmap* bmp, int rowWidth) { + unsigned min_dimension; + unsigned interleave_mask, interleaved_bits; + unsigned shifted_mask, shift_bits; + unsigned lo_Y, hi_Y, Y; + unsigned lo_X, hi_X, X; + Twiddle_CalcFactors(bmp->width, bmp->height); + + for (int y = 0; y < bmp->height; y++) + { + Twiddle_CalcY(y); + cc_uint8* src = (cc_uint8*)(bmp->scan0 + y * rowWidth); + + for (int x = 0; x < bmp->width; x++, src += 4) + { + Twiddle_CalcX(x); + dst[X | Y] = BGRA8_to_BGRA4(src); + } + } +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + GLuint texId = gldcGenTexture(); + gldcBindTexture(texId); + + int res = gldcAllocTexture(bmp->width, bmp->height, PVR_TXRFMT_ARGB4444); + if (res) { Platform_LogConst("Out of PVR VRAM!"); return 0; } + + void* pixels; + int width, height; + gldcGetTexture(&pixels, &width, &height); + ConvertTexture(pixels, bmp, rowWidth); + return texId; +} + +// TODO: struct GPUTexture ?? +static void ConvertSubTexture(cc_uint16* dst, int texWidth, int texHeight, + int originX, int originY, + struct Bitmap* bmp, int rowWidth) { + unsigned min_dimension; + unsigned interleave_mask, interleaved_bits; + unsigned shifted_mask, shift_bits; + unsigned lo_Y, hi_Y, Y; + unsigned lo_X, hi_X, X; + Twiddle_CalcFactors(texWidth, texHeight); + + for (int y = 0; y < bmp->height; y++) + { + int dstY = y + originY; + Twiddle_CalcY(dstY); + cc_uint8* src = (cc_uint8*)(bmp->scan0 + rowWidth * y); + + for (int x = 0; x < bmp->width; x++, src += 4) + { + int dstX = x + originX; + Twiddle_CalcX(dstX); + dst[X | Y] = BGRA8_to_BGRA4(src); + } + } +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + gldcBindTexture(texId); + + void* pixels; + int width, height; + gldcGetTexture(&pixels, &width, &height); + + ConvertSubTexture(pixels, width, height, + x, y, part, rowWidth); + // TODO: Do we need to flush VRAM? +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_fogColor; +static float gfx_fogEnd = 16.0f, gfx_fogDensity = 1.0f; +static FogFunc gfx_fogMode = -1; + +void Gfx_SetFog(cc_bool enabled) { + gfx_fogEnabled = enabled; + if (FOG_ENABLED == enabled) return; + + FOG_ENABLED = enabled; + STATE_DIRTY = GL_TRUE; +} + +void Gfx_SetFogCol(PackedCol color) { + if (color == gfx_fogColor) return; + gfx_fogColor = color; + + float r = PackedCol_R(color) / 255.0f; + float g = PackedCol_G(color) / 255.0f; + float b = PackedCol_B(color) / 255.0f; + float a = PackedCol_A(color) / 255.0f; + + pvr_fog_table_color(a, r, g, b); +} + +static void UpdateFog(void) { + if (gfx_fogMode == FOG_LINEAR) { + pvr_fog_table_linear(0.0f, gfx_fogEnd); + } else if (gfx_fogMode == FOG_EXP) { + pvr_fog_table_exp(gfx_fogDensity); + } else if (gfx_fogMode == FOG_EXP2) { + pvr_fog_table_exp2(gfx_fogDensity); + } +} + +void Gfx_SetFogDensity(float value) { + if (value == gfx_fogDensity) return; + gfx_fogDensity = value; + UpdateFog(); +} + +void Gfx_SetFogEnd(float value) { + if (value == gfx_fogEnd) return; + gfx_fogEnd = value; + UpdateFog(); +} + +void Gfx_SetFogMode(FogFunc func) { + if (func == gfx_fogMode) return; + gfx_fogMode = func; + UpdateFog(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static matrix_t __attribute__((aligned(32))) _proj, _view; +static float textureOffsetX, textureOffsetY; +static int textureOffset; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_PROJECTION) memcpy(&_proj, matrix, sizeof(struct Matrix)); + if (type == MATRIX_VIEW) memcpy(&_view, matrix, sizeof(struct Matrix)); + + mat_load( &_proj); + mat_apply(&_view); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + + +void Gfx_EnableTextureOffset(float x, float y) { + textureOffset = true; + textureOffsetX = x; + textureOffsetY = y; +} + +void Gfx_DisableTextureOffset(void) { + textureOffset = false; +} + +static CC_NOINLINE void ShiftTextureCoords(int count) { + for (int i = 0; i < count; i++) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + i; + v->U += textureOffsetX; + v->V += textureOffsetY; + } +} + +static CC_NOINLINE void UnshiftTextureCoords(int count) { + for (int i = 0; i < count; i++) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + i; + v->U -= textureOffsetX; + v->V -= textureOffsetY; + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------State setup-------------------------------------------------------* +*#########################################################################################################################*/ +static void Gfx_FreeState(void) { FreeDefaultResources(); } +static void Gfx_RestoreState(void) { + InitDefaultResources(); + gfx_format = -1; +} + +cc_bool Gfx_WarnIfNecessary(void) { + return false; +} + + +/*########################################################################################################################* +*----------------------------------------------------------Drawing--------------------------------------------------------* +*#########################################################################################################################*/ +extern void apply_poly_header(PolyHeader* header, PolyList* activePolyList); + +extern Vertex* DrawColouredQuads(const void* src, Vertex* dst, int numQuads); +extern Vertex* DrawTexturedQuads(const void* src, Vertex* dst, int numQuads); + +void DrawQuads(int count, void* src) { + if (!count) return; + PolyList* output = _glActivePolyList(); + AlignedVectorHeader* hdr = &output->vector.hdr; + + uint32_t header_required = (hdr->size == 0) || STATE_DIRTY; + // Reserve room for the vertices and header + Vertex* beg = aligned_vector_reserve(&output->vector, hdr->size + (header_required) + count); + + if (header_required) { + apply_poly_header((PolyHeader*)beg, output); + STATE_DIRTY = GL_FALSE; + beg++; + hdr->size += 1; + } + Vertex* end; + + if (TEXTURES_ENABLED) { + end = DrawTexturedQuads(src, beg, count >> 2); + } else { + end = DrawColouredQuads(src, beg, count >> 2); + } + hdr->size += (end - beg); +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + TEXTURES_ENABLED = fmt == VERTEX_FORMAT_TEXTURED; + STATE_DIRTY = GL_TRUE; +} + +void Gfx_DrawVb_Lines(int verticesCount) { + //SetupVertices(0); + //glDrawArrays(GL_LINES, 0, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + void* src; + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + src = gfx_vertices + startVertex * SIZEOF_VERTEX_TEXTURED; + } else { + src = gfx_vertices + startVertex * SIZEOF_VERTEX_COLOURED; + } + + DrawQuads(verticesCount, src); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + if (textureOffset) ShiftTextureCoords(verticesCount); + DrawQuads(verticesCount, gfx_vertices); + if (textureOffset) UnshiftTextureCoords(verticesCount); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + if (renderingDisabled) return; + + void* src = gfx_vertices + startVertex * SIZEOF_VERTEX_TEXTURED; + DrawQuads(verticesCount, src); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + int freeMem = _glFreeTextureMemory(); + int usedMem = _glUsedTextureMemory(); + + float freeMemMB = freeMem / (1024.0 * 1024.0); + float usedMemMB = usedMem / (1024.0 * 1024.0); + + String_AppendConst(info, "-- Using Dreamcast --\n"); + String_AppendConst(info, "GPU: PowerVR2 CLX2 100mHz\n"); + String_AppendConst(info, "T&L: GLdc library (KallistiOS / Kazade)\n"); + String_Format2(info, "Texture memory: %f2 MB used, %f2 MB free\n", &usedMemMB, &freeMemMB); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { } + +void Gfx_ClearBuffers(GfxBuffers buffers) { + // TODO clear only some buffers + // no need to use glClear +} + +void Gfx_EndFrame(void) { + pvr_wait_ready(); + glKosSwapBuffers(); + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_OnWindowResize(void) { + Gfx_SetViewport(0, 0, Game.Width, Game.Height); +} + +extern float VP_COL_HWIDTH, VP_TEX_HWIDTH; +extern float VP_COL_HHEIGHT, VP_TEX_HHEIGHT; +extern float VP_COL_X_PLUS_HWIDTH, VP_TEX_X_PLUS_HWIDTH; +extern float VP_COL_Y_PLUS_HHEIGHT, VP_TEX_Y_PLUS_HHEIGHT; + +void Gfx_SetViewport(int x, int y, int w, int h) { + if (x == 0 && y == 0 && w == Game.Width && h == Game.Height) { + SCISSOR_TEST_ENABLED = GL_FALSE; + } else { + SCISSOR_TEST_ENABLED = GL_TRUE; + } + STATE_DIRTY = GL_TRUE; + + glViewport(x, y, w, h); + glScissor (x, y, w, h); + + VP_COL_HWIDTH = VP_TEX_HWIDTH = w * 0.5f; + VP_COL_HHEIGHT = VP_TEX_HHEIGHT = h * -0.5f; + + VP_COL_X_PLUS_HWIDTH = VP_TEX_X_PLUS_HWIDTH = x + w * 0.5f; + VP_COL_Y_PLUS_HHEIGHT = VP_TEX_Y_PLUS_HHEIGHT = y + h * 0.5f; +} +#endif diff --git a/src/Graphics_GCWii.c b/src/Graphics_GCWii.c new file mode 100644 index 0000000..d894271 --- /dev/null +++ b/src/Graphics_GCWii.c @@ -0,0 +1,598 @@ +#include "Core.h" +#if defined CC_BUILD_GCWII +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include +#include +#include + +static void* fifo_buffer; +#define FIFO_SIZE (256 * 1024) +extern void* Window_XFB; +static void* xfbs[2]; +static int curFB; +static GfxResourceID white_square; +// https://wiibrew.org/wiki/Developer_tips +// https://devkitpro.org/wiki/libogc/GX + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static void InitGX(void) { + GXRModeObj* mode = VIDEO_GetPreferredMode(NULL); + fifo_buffer = MEM_K0_TO_K1(memalign(32, FIFO_SIZE)); + memset(fifo_buffer, 0, FIFO_SIZE); + + GX_Init(fifo_buffer, FIFO_SIZE); + Gfx_SetViewport(0, 0, mode->fbWidth, mode->efbHeight); + GX_SetDispCopyYScale((f32)mode->xfbHeight / (f32)mode->efbHeight); + GX_SetDispCopySrc(0, 0, mode->fbWidth, mode->efbHeight); + GX_SetDispCopyDst(mode->fbWidth, mode->xfbHeight); + GX_SetCopyFilter(mode->aa, mode->sample_pattern, + GX_TRUE, mode->vfilter); + GX_SetFieldMode(mode->field_rendering, + ((mode->viHeight==2*mode->xfbHeight)?GX_ENABLE:GX_DISABLE)); + + GX_SetCullMode(GX_CULL_NONE); + GX_SetDispCopyGamma(GX_GM_1_0); + GX_InvVtxCache(); + + GX_SetNumChans(1); + + xfbs[0] = Window_XFB; + xfbs[1] = MEM_K0_TO_K1(SYS_AllocateFramebuffer(mode)); +} + +void Gfx_Create(void) { + if (!Gfx.Created) InitGX(); + + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.MaxTexSize = 512 * 512; + + Gfx.MinTexWidth = 4; + Gfx.MinTexHeight = 4; + Gfx.Created = true; + gfx_vsync = true; + + Gfx_RestoreState(); +} + +void Gfx_Free(void) { + Gfx_FreeState(); + GX_AbortFrame(); + //GX_Flush(); // TODO needed? + VIDEO_Flush(); +} +cc_bool Gfx_TryRestoreContext(void) { return true; } + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 4x4 dummy white texture (textures must be at least 1 4x4 tile) + struct Bitmap bmp; + BitmapCol pixels[4 * 4]; + Mem_Set(pixels, 0xFF, sizeof(pixels)); + Bitmap_Init(bmp, 4, 4, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCTexture_ { + GXTexObj obj; + cc_uint32 pixels[]; +} CCTexture; + +// ClassiCube RGBA8 bitmaps +// - store pixels in simple linear order +// i.e. pixels are ordered as (x=0,y=0), (x=1,y=0), (x=2,y=0) ... (x=0,y=1) ... +// - store colour components in interleaved order +// i.e. pixels are stored in memory as ARGB ARGB ARGB ARGB ... +// GX RGBA8 textures +// - store pixels in 4x4 tiles +// - store all of the AR values of the tile's pixels, then store all of the GB values +static void ReorderPixels(CCTexture* tex, struct Bitmap* bmp, + int originX, int originY, int rowWidth) { + int stride = GX_GetTexObjWidth(&tex->obj) * 4; + // TODO not really right + // TODO originX ignored + originX &= ~0x03; + originY &= ~0x03; + + // http://hitmen.c02.at/files/yagcd/yagcd/chap15.html + // section 15.35 TPL (Texture Palette) + // "RGBA8 (4x4 tiles in two cache lines - first is AR and second is GB" + uint8_t *src = (uint8_t*)bmp->scan0; + uint8_t *dst = (uint8_t*)tex->pixels + stride * originY; + int srcWidth = bmp->width, srcHeight = bmp->height; + + for (int tileY = 0; tileY < srcHeight; tileY += 4) + for (int tileX = 0; tileX < srcWidth; tileX += 4) + { + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + uint32_t idx = (((tileY + y) * rowWidth) + tileX + x) << 2; + + *dst++ = src[idx + 0]; // A + *dst++ = src[idx + 1]; // R + } + } + + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + uint32_t idx = (((tileY + y) * rowWidth) + tileX + x) << 2; + + *dst++ = src[idx + 2]; // G + *dst++ = src[idx + 3]; // B + } + } + } +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + int size = bmp->width * bmp->height * 4; + CCTexture* tex = (CCTexture*)memalign(32, 32 + size); + if (!tex) return NULL; + + GX_InitTexObj(&tex->obj, tex->pixels, bmp->width, bmp->height, + GX_TF_RGBA8, GX_REPEAT, GX_REPEAT, GX_FALSE); + GX_InitTexObjFilterMode(&tex->obj, GX_NEAR, GX_NEAR); + + ReorderPixels(tex, bmp, 0, 0, rowWidth); + DCFlushRange(tex->pixels, size); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)texId; + // TODO: wrong behaviour if x/y/part isn't multiple of 4 pixels + ReorderPixels(tex, part, x, y, rowWidth); + GX_InvalidateTexAll(); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GfxResourceID data = *texId; + if (data) Mem_Free(data); + *texId = NULL; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + +void Gfx_BindTexture(GfxResourceID texId) { + CCTexture* tex = (CCTexture*)texId; + if (!tex) tex = white_square; + + GX_LoadTexObj(&tex->obj, GX_TEXMAP0); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static GXColor gfx_clearColor = { 0, 0, 0, 255 }; + +void Gfx_SetFaceCulling(cc_bool enabled) { + // NOTE: seems like ClassiCube's triangle ordering is opposite of what GX considers front facing + GX_SetCullMode(enabled ? GX_CULL_FRONT : GX_CULL_NONE); +} + +static void SetAlphaBlend(cc_bool enabled) { + if (enabled) { + GX_SetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR); + } else { + GX_SetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_CLEAR); + } +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { +} + +void Gfx_ClearColor(PackedCol color) { + gfx_clearColor.r = PackedCol_R(color); + gfx_clearColor.g = PackedCol_G(color); + gfx_clearColor.b = PackedCol_B(color); + + GX_SetCopyClear(gfx_clearColor, 0x00ffffff); // TODO: use GX_MAX_Z24 +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +static cc_bool depth_write = true, depth_test = true; +static void UpdateDepthState(void) { + GX_SetZMode(depth_test, GX_LEQUAL, depth_write); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + depth_write = enabled; + UpdateDepthState(); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + depth_test = enabled; + UpdateDepthState(); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +static BitmapCol* GCWii_GetRow(struct Bitmap* bmp, int y, void* ctx) { + u8* buffer = (u8*)ctx; + u8 a, r, g, b; + int blockYStride = 4 * (bmp->width * 4); // tile row stride = 4 * row stride + int blockXStride = (4 * 4) * 4; // 16 pixels per tile + + // Do the inverse of converting from 4x4 tiled to linear + for (u32 x = 0; x < bmp->width; x++){ + int tileY = y >> 2, tileX = x >> 2; + int locY = y & 0x3, locX = x & 0x3; + int idx = (tileY * blockYStride) + (tileX * blockXStride) + ((locY << 2) + locX) * 2; + + // All 16 pixels are stored with AR first, then GB + //a = buffer[idx ]; + r = buffer[idx + 1]; + g = buffer[idx + 32]; + b = buffer[idx + 33]; + + bmp->scan0[x] = BitmapColor_RGB(r, g, b); + } + return bmp->scan0; +} + +cc_result Gfx_TakeScreenshot(struct Stream* output) { + BitmapCol tmp[1024]; + GXRModeObj* vmode = VIDEO_GetPreferredMode(NULL); + int width = vmode->fbWidth; + int height = vmode->efbHeight; + + u8* buffer = memalign(32, width * height * 4); + if (!buffer) return ERR_OUT_OF_MEMORY; + + GX_SetTexCopySrc(0, 0, width, height); + GX_SetTexCopyDst(width, height, GX_TF_RGBA8, GX_FALSE); + GX_CopyTex(buffer, GX_FALSE); + GX_PixModeSync(); + GX_Flush(); + DCFlushRange(buffer, width * height * 4); + + struct Bitmap bmp; + bmp.scan0 = tmp; + bmp.width = width; + bmp.height = height; + + cc_result res = Png_Encode(&bmp, output, GCWii_GetRow, false, buffer); + free(buffer); + return res; +} + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using GC/Wii --\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + // TODO clear only some buffers +} + +void Gfx_EndFrame(void) { + curFB ^= 1; // swap "front" and "back" buffers + GX_CopyDisp(xfbs[curFB], GX_TRUE); + GX_DrawDone(); + + VIDEO_SetNextFramebuffer(xfbs[curFB]); + VIDEO_Flush(); + + if (gfx_vsync) VIDEO_WaitVSync(); + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_OnWindowResize(void) { } + +void Gfx_SetViewport(int x, int y, int w, int h) { + GX_SetViewport(x, y, w, h, 0, 1); + GX_SetScissor(x, y, w, h); +} + +cc_bool Gfx_WarnIfNecessary(void) { return false; } + + +/*########################################################################################################################* +*-------------------------------------------------------Index Buffers-----------------------------------------------------* +*#########################################################################################################################*/ +//static cc_uint16 __attribute__((aligned(16))) gfx_indices[GFX_MAX_INDICES]; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + //fillFunc(gfx_indices, count, obj); + // not used since render using GX_QUADS anyways + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*------------------------------------------------------Vertex Buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint8* gfx_vertices; +static int vb_size; + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return memalign(16, count * strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; + DCFlushRange(vb, vb_size); +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return memalign(16, maxVertices * strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; + DCFlushRange(vb, vb_size); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_fogColor; +static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f; +static int gfx_fogMode = -1; + +void Gfx_SetFog(cc_bool enabled) { + gfx_fogEnabled = enabled; +} + +void Gfx_SetFogCol(PackedCol color) { + if (color == gfx_fogColor) return; + gfx_fogColor = color; +} + +void Gfx_SetFogDensity(float value) { +} + +void Gfx_SetFogEnd(float value) { + if (value == gfx_fogEnd) return; + gfx_fogEnd = value; +} + +void Gfx_SetFogMode(FogFunc func) { +} + +static void SetAlphaTest(cc_bool enabled) { + if (enabled) { + GX_SetAlphaCompare(GX_GREATER, 127, GX_AOP_AND, GX_ALWAYS, 0); + } else { + GX_SetAlphaCompare(GX_ALWAYS, 0, GX_AOP_AND, GX_ALWAYS, 0); + } + + // See explanation from libGX headers + // Normally, Z buffering should happen before texturing, as this enables better performance by not texturing pixels that are not visible; + // however, when alpha compare is used, Z buffering must be done after texturing + // Parameter[0] = Enables Z-buffering before texturing when set to GX_TRUE; otherwise, Z-buffering takes place after texturing. + GX_SetZCompLoc(!enabled); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + GX_SetColorUpdate(!depthOnly); + GX_SetAlphaUpdate(!depthOnly); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // Transposed, source guOrtho https://github.com/devkitPro/libogc/blob/master/libogc/gu.c + // The simplified calculation below uses: L = 0, R = width, T = 0, B = height + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -1.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -zFar / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + // Transposed, source guPersepctive https://github.com/devkitPro/libogc/blob/master/libogc/gu.c + *matrix = Matrix_Identity; + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; +} + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + const float* m = (const float*)matrix; + float tmp[16]; + + // Transpose matrix + for (int i = 0; i < 4; i++) + { + tmp[i * 4 + 0] = m[0 + i]; + tmp[i * 4 + 1] = m[4 + i]; + tmp[i * 4 + 2] = m[8 + i]; + tmp[i * 4 + 3] = m[12 + i]; + } + + if (type == MATRIX_PROJECTION) { + GX_LoadProjectionMtx(tmp, + tmp[3*4+3] == 0.0f ? GX_PERSPECTIVE : GX_ORTHOGRAPHIC); + } else { + GX_LoadPosMtxImm(tmp, GX_PNMTX0); + } +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} +static float texOffsetX, texOffsetY; +static void UpdateTexCoordGen(void) { + if (texOffsetX || texOffsetY) { + Mtx mat = { 0 }; + // https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glTranslate.xml + mat[0][0] = 1; mat[0][3] = texOffsetX; + mat[1][1] = 1; mat[1][3] = texOffsetY; + + GX_LoadTexMtxImm(mat, GX_TEXMTX0, GX_MTX2x4); + GX_SetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0); + } else { + GX_SetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY); + } +} + +void Gfx_EnableTextureOffset(float x, float y) { + texOffsetX = x; texOffsetY = y; + UpdateTexCoordGen(); +} + +void Gfx_DisableTextureOffset(void) { + texOffsetX = 0; texOffsetY = 0; + UpdateTexCoordGen(); +} + + + +/*########################################################################################################################* +*---------------------------------------------------------Drawing---------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + GX_ClearVtxDesc(); + if (fmt == VERTEX_FORMAT_TEXTURED) { + GX_SetVtxDesc(GX_VA_POS, GX_DIRECT); + GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GX_SetVtxDesc(GX_VA_TEX0, GX_DIRECT); + + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + + GX_SetNumTexGens(1); + // TODO avoid calling here. only call in Gfx_Init and Enable/Disable Texture Offset ??? + UpdateTexCoordGen(); + GX_SetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GX_SetTevOp(GX_TEVSTAGE0, GX_MODULATE); + } else { + GX_SetVtxDesc(GX_VA_POS, GX_DIRECT); + GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT); + + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GX_SetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + + GX_SetNumTexGens(0); + GX_SetTevOrder(GX_TEVSTAGE0, GX_TEXCOORDNULL, GX_TEXMAP_NULL, GX_COLOR0A0); + GX_SetTevOp(GX_TEVSTAGE0, GX_PASSCLR); + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { +} + + +static void Draw_ColouredTriangles(int verticesCount, int startVertex) { + GX_Begin(GX_QUADS, GX_VTXFMT0, verticesCount); + // TODO: Ditch indexed rendering and use GX_QUADS instead ?? + for (int i = 0; i < verticesCount; i++) + { + struct VertexColoured* v = (struct VertexColoured*)gfx_vertices + startVertex + i; + + GX_Position3f32(v->x, v->y, v->z); + GX_Color1u32(v->Col); + } + GX_End(); +} + +static void Draw_TexturedTriangles(int verticesCount, int startVertex) { + GX_Begin(GX_QUADS, GX_VTXFMT0, verticesCount); + for (int i = 0; i < verticesCount; i++) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + GX_Position3f32(v->x, v->y, v->z); + GX_Color1u32(v->Col); + GX_TexCoord2f32(v->U, v->V); + } + GX_End(); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + Draw_TexturedTriangles(verticesCount, startVertex); + } else { + Draw_ColouredTriangles(verticesCount, startVertex); + } +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + Draw_TexturedTriangles(verticesCount, 0); + } else { + Draw_ColouredTriangles(verticesCount, 0); + } +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + Draw_TexturedTriangles(verticesCount, startVertex); +} +#endif diff --git a/src/Graphics_GL1.c b/src/Graphics_GL1.c new file mode 100644 index 0000000..5819014 --- /dev/null +++ b/src/Graphics_GL1.c @@ -0,0 +1,691 @@ +/* Silence deprecation warnings on modern macOS/iOS */ +#define GL_SILENCE_DEPRECATION +#define GLES_SILENCE_DEPRECATION + +#include "Core.h" +#if CC_GFX_BACKEND == CC_GFX_BACKEND_GL1 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#ifdef CC_BUILD_WIN + #define CC_BUILD_GL11_FALLBACK +#endif + +/* The OpenGL backend is a bit of a mess, since it's really 2 backends in one: + * - OpenGL 1.1 (completely lacking GPU, fallbacks to say Windows built-in software rasteriser) + * - OpenGL 1.5 or OpenGL 1.2 + GL_ARB_vertex_buffer_object (default desktop backend) +*/ +#include "../misc/opengl/GLCommon.h" + +/* e.g. GLAPI void APIENTRY glFunction(int args); */ +#define GL_FUNC(_retType, name) GLAPI _retType APIENTRY name +#include "../misc/opengl/GL1Funcs.h" + +#if defined CC_BUILD_GL11 +static GLuint activeList; +#define gl_DYNAMICLISTID 1234567891 +static void* dynamicListData; +static cc_uint16 gl_indices[GFX_MAX_INDICES]; +#else +/* OpenGL functions use stdcall instead of cdecl on Windows */ +static void (APIENTRY *_glBindBuffer)(GLenum target, GfxResourceID buffer); /* NOTE: buffer is actually a GLuint in OpenGL */ +static void (APIENTRY *_glDeleteBuffers)(GLsizei n, const GLuint *buffers); +static void (APIENTRY *_glGenBuffers)(GLsizei n, GLuint *buffers); +static void (APIENTRY *_glBufferData)(GLenum target, cc_uintptr size, const GLvoid* data, GLenum usage); +static void (APIENTRY *_glBufferSubData)(GLenum target, cc_uintptr offset, cc_uintptr size, const GLvoid* data); +#endif + +static void GLContext_GetAll(const struct DynamicLibSym* syms, int count) { + int i; + for (i = 0; i < count; i++) + { + *syms[i].symAddr = GLContext_GetAddress(syms[i].name); + } +} + + +#if defined CC_BUILD_GL11_FALLBACK && !defined CC_BUILD_GL11 +/* Note the following about calling OpenGL functions on Windows */ +/* 1) wglGetProcAddress returns a context specific address */ +/* 2) dllimport functions are implemented using indirect function pointers */ +/* https://web.archive.org/web/20080321171626/http://blogs.msdn.com/oldnewthing/archive/2006/07/20/672695.aspx */ +/* https://web.archive.org/web/20071016185327/http://blogs.msdn.com/oldnewthing/archive/2006/07/27/680250.aspx */ +/* Therefore one layer of indirection can be avoided by calling wglGetProcAddress functions instead */ +/* e.g. if _glDrawElements = wglGetProcAddress("glDrawElements") */ +/* call [glDrawElements] --> opengl32.dll thunk--> GL driver thunk --> GL driver implementation */ +/* call [_glDrawElements] --> GL driver thunk --> GL driver implementation */ + +/* e.g. typedef void (APIENTRY *FP_glFunction)(int args); */ +#undef GL_FUNC +#define GL_FUNC(_retType, name) typedef _retType (APIENTRY *FP_ ## name) +#include "../misc/opengl/GL1Funcs.h" + +/* e.g. static void (APIENTRY *_glFunction)(int args); */ +#undef GL_FUNC +#define GL_FUNC(_retType, name) static _retType (APIENTRY *_ ## name) +#include "../misc/opengl/GL1Funcs.h" + +#define GLSym(sym) { DYNAMICLIB_QUOTE(sym), (void**)&_ ## sym } +static const struct DynamicLibSym coreFuncs[] = { + GLSym(glColorPointer), GLSym(glTexCoordPointer), GLSym(glVertexPointer), + + GLSym(glDrawArrays), GLSym(glDrawElements), + + GLSym(glBindTexture), GLSym(glDeleteTextures), GLSym(glGenTextures), + GLSym(glTexImage2D), GLSym(glTexSubImage2D), +}; + +static void LoadCoreFuncs(void) { + GLContext_GetAll(coreFuncs, Array_Elems(coreFuncs)); +} +#else +#define _glColorPointer glColorPointer +#define _glTexCoordPointer glTexCoordPointer +#define _glVertexPointer glVertexPointer + +#define _glDrawArrays glDrawArrays +#define _glDrawElements glDrawElements + +#define _glBindTexture glBindTexture +#define _glDeleteTextures glDeleteTextures +#define _glGenTextures glGenTextures +#define _glTexImage2D glTexImage2D +#define _glTexSubImage2D glTexSubImage2D +#endif + +typedef void (*GL_SetupVBFunc)(void); +typedef void (*GL_SetupVBRangeFunc)(int startVertex); +static GL_SetupVBFunc gfx_setupVBFunc; +static GL_SetupVBRangeFunc gfx_setupVBRangeFunc; +#include "_GLShared.h" + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +#ifndef CC_BUILD_GL11 +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + cc_uint16 indices[GFX_MAX_INDICES]; + GfxResourceID id = NULL; + cc_uint32 size = count * sizeof(cc_uint16); + + _glGenBuffers(1, (GLuint*)&id); + fillFunc(indices, count, obj); + _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, id); + _glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, indices, GL_STATIC_DRAW); + return id; +} + +void Gfx_BindIb(GfxResourceID ib) { _glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib); } + +void Gfx_DeleteIb(GfxResourceID* ib) { + GfxResourceID id = *ib; + if (!id) return; + + _glDeleteBuffers(1, (GLuint*)&id); + *ib = 0; +} +#else +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { return 0; } +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } +#endif + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +#ifndef CC_BUILD_GL11 +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + GfxResourceID id = NULL; + _glGenBuffers(1, (GLuint*)&id); + _glBindBuffer(GL_ARRAY_BUFFER, id); + return id; +} + +void Gfx_BindVb(GfxResourceID vb) { + _glBindBuffer(GL_ARRAY_BUFFER, vb); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID id = *vb; + if (id) _glDeleteBuffers(1, (GLuint*)&id); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return FastAllocTempMem(count * strideSizes[fmt]); +} + +void Gfx_UnlockVb(GfxResourceID vb) { + _glBufferData(GL_ARRAY_BUFFER, tmpSize, tmpData, GL_STATIC_DRAW); +} +#else +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return glGenLists(1); +} +void Gfx_BindVb(GfxResourceID vb) { activeList = ptr_to_uint(vb); } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GLuint id = ptr_to_uint(*vb); + if (id) glDeleteLists(id, 1); + *vb = 0; +} + +static void UpdateDisplayList(GLuint list, void* vertices, VertexFormat fmt, int count) { + /* We need to restore client state afer building the list */ + int realFormat = gfx_format; + void* dyn_data = dynamicListData; + Gfx_SetVertexFormat(fmt); + dynamicListData = vertices; + + glNewList(list, GL_COMPILE); + gfx_setupVBFunc(); + glDrawElements(GL_TRIANGLES, ICOUNT(count), GL_UNSIGNED_SHORT, gl_indices); + glEndList(); + + Gfx_SetVertexFormat(realFormat); + dynamicListData = dyn_data; +} + +/* NOTE! Building chunk in Builder.c relies on vb being ignored */ +/* If that changes, you must fix Builder.c to properly call Gfx_LockVb */ +static VertexFormat tmpFormat; +static int tmpCount; +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + tmpFormat = fmt; + tmpCount = count; + return FastAllocTempMem(count * strideSizes[fmt]); +} + +void Gfx_UnlockVb(GfxResourceID vb) { + UpdateDisplayList((GLuint)vb, tmpData, tmpFormat, tmpCount); +} + +GfxResourceID Gfx_CreateVb2(void* vertices, VertexFormat fmt, int count) { + GLuint list = glGenLists(1); + UpdateDisplayList(list, vertices, fmt, count); + return list; +} +#endif + + +/*########################################################################################################################* +*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------* +*#########################################################################################################################*/ +#ifndef CC_BUILD_GL11 +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + GfxResourceID id = NULL; + cc_uint32 size = maxVertices * strideSizes[fmt]; + + _glGenBuffers(1, (GLuint*)&id); + _glBindBuffer(GL_ARRAY_BUFFER, id); + _glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_DYNAMIC_DRAW); + return id; +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { + _glBindBuffer(GL_ARRAY_BUFFER, vb); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { + GfxResourceID id = *vb; + if (id) _glDeleteBuffers(1, (GLuint*)&id); + *vb = 0; +} + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return FastAllocTempMem(count * strideSizes[fmt]); +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + _glBindBuffer(GL_ARRAY_BUFFER, vb); + _glBufferSubData(GL_ARRAY_BUFFER, 0, tmpSize, tmpData); +} + +void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount) { + cc_uint32 size = vCount * gfx_stride; + _glBindBuffer(GL_ARRAY_BUFFER, vb); + _glBufferSubData(GL_ARRAY_BUFFER, 0, size, vertices); +} +#else +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return (GfxResourceID)Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { + activeList = gl_DYNAMICLISTID; + dynamicListData = vb; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { + void* addr = *vb; + if (addr) Mem_Free(addr); + *vb = 0; +} + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { return vb; } +void Gfx_UnlockDynamicVb(GfxResourceID vb) { Gfx_BindDynamicVb(vb); } + +void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount) { + Gfx_BindDynamicVb(vb); + Mem_Copy(vb, vertices, vCount * gfx_stride); +} +#endif + + +/*########################################################################################################################* +*----------------------------------------------------------Drawing--------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_GL11 + /* point to client side dynamic array */ + #define VB_PTR ((cc_uint8*)dynamicListData) + #define IB_PTR gl_indices +#else + /* no client side array, use vertex buffer object */ + #define VB_PTR 0 + #define IB_PTR NULL +#endif + +static void GL_SetupVbColoured(void) { + _glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_COLOURED, VB_PTR + 0); + _glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_COLOURED, VB_PTR + 12); +} + +static void GL_SetupVbTextured(void) { + _glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, VB_PTR + 0); + _glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, VB_PTR + 12); + _glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, VB_PTR + 16); +} + +static void GL_SetupVbColoured_Range(int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_COLOURED; + _glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_COLOURED, VB_PTR + offset + 0); + _glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_COLOURED, VB_PTR + offset + 12); +} + +static void GL_SetupVbTextured_Range(int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + _glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, VB_PTR + offset + 0); + _glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, VB_PTR + offset + 12); + _glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, VB_PTR + offset + 16); +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnable(GL_TEXTURE_2D); + + gfx_setupVBFunc = GL_SetupVbTextured; + gfx_setupVBRangeFunc = GL_SetupVbTextured_Range; + } else { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisable(GL_TEXTURE_2D); + + gfx_setupVBFunc = GL_SetupVbColoured; + gfx_setupVBRangeFunc = GL_SetupVbColoured_Range; + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { + gfx_setupVBFunc(); + _glDrawArrays(GL_LINES, 0, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { +#ifdef CC_BUILD_GL11 + if (activeList != gl_DYNAMICLISTID) { glCallList(activeList); return; } +#endif + gfx_setupVBRangeFunc(startVertex); + _glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, IB_PTR); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { +#ifdef CC_BUILD_GL11 + if (activeList != gl_DYNAMICLISTID) { glCallList(activeList); return; } +#endif + gfx_setupVBFunc(); + _glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, IB_PTR); +} + +#ifdef CC_BUILD_GL11 +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { glCallList(activeList); } +#else +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + _glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, VB_PTR + offset + 0); + _glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, VB_PTR + offset + 12); + _glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, VB_PTR + offset + 16); + _glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, IB_PTR); +} +#endif /* !CC_BUILD_GL11 */ + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_BindTexture(GfxResourceID texId) { + _glBindTexture(GL_TEXTURE_2D, ptr_to_uint(texId)); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_fogColor; +static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f; +static int gfx_fogMode = -1; + +void Gfx_SetFog(cc_bool enabled) { + gfx_fogEnabled = enabled; + if (enabled) { glEnable(GL_FOG); } else { glDisable(GL_FOG); } +} + +void Gfx_SetFogCol(PackedCol color) { + float rgba[4]; + if (color == gfx_fogColor) return; + + rgba[0] = PackedCol_R(color) / 255.0f; + rgba[1] = PackedCol_G(color) / 255.0f; + rgba[2] = PackedCol_B(color) / 255.0f; + rgba[3] = PackedCol_A(color) / 255.0f; + + glFogfv(GL_FOG_COLOR, rgba); + gfx_fogColor = color; +} + +void Gfx_SetFogDensity(float value) { + if (value == gfx_fogDensity) return; + glFogf(GL_FOG_DENSITY, value); + gfx_fogDensity = value; +} + +void Gfx_SetFogEnd(float value) { + if (value == gfx_fogEnd) return; + glFogf(GL_FOG_END, value); + gfx_fogEnd = value; +} + +void Gfx_SetFogMode(FogFunc func) { + static GLint modes[3] = { GL_LINEAR, GL_EXP, GL_EXP2 }; + if (func == gfx_fogMode) return; + +#ifdef CC_BUILD_GLES + /* OpenGL ES doesn't support glFogi, so use glFogf instead */ + /* https://www.khronos.org/registry/OpenGL-Refpages/es1.1/xhtml/ */ + glFogf(GL_FOG_MODE, modes[func]); +#else + glFogi(GL_FOG_MODE, modes[func]); +#endif + gfx_fogMode = func; +} + +static void SetAlphaTest(cc_bool enabled) { + if (enabled) { glEnable(GL_ALPHA_TEST); } else { glDisable(GL_ALPHA_TEST); } +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); + if (enabled) { glEnable(GL_TEXTURE_2D); } else { glDisable(GL_TEXTURE_2D); } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static GLenum matrix_modes[3] = { GL_PROJECTION, GL_MODELVIEW, GL_TEXTURE }; +static int lastMatrix; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadMatrixf((const float*)matrix); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadIdentity(); +} + +static struct Matrix texMatrix = Matrix_IdentityValue; +void Gfx_EnableTextureOffset(float x, float y) { + texMatrix.row4.x = x; texMatrix.row4.y = y; + Gfx_LoadMatrix(2, &texMatrix); +} + +void Gfx_DisableTextureOffset(void) { Gfx_LoadIdentityMatrix(2); } + + +/*########################################################################################################################* +*-------------------------------------------------------State setup-------------------------------------------------------* +*#########################################################################################################################*/ +static void Gfx_FreeState(void) { FreeDefaultResources(); } +static void Gfx_RestoreState(void) { + InitDefaultResources(); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + gfx_format = -1; + + glHint(GL_FOG_HINT, GL_NICEST); + glAlphaFunc(GL_GREATER, 0.5f); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthFunc(GL_LEQUAL); +} + +cc_bool Gfx_WarnIfNecessary(void) { + cc_string renderer = String_FromReadonly((const char*)glGetString(GL_RENDERER)); + +#ifdef CC_BUILD_GL11 + Chat_AddRaw("&cYou are using the very outdated OpenGL backend."); + Chat_AddRaw("&cAs such you may experience poor performance."); + Chat_AddRaw("&cIt is likely you need to install video card drivers."); +#endif + + if (String_ContainsConst(&renderer, "llvmpipe")) { + Chat_AddRaw("&cSoftware rendering is being used, performance will greatly suffer."); + Chat_AddRaw("&cVSync may not work, and you may see disappearing clouds and map edges."); + return true; + } + if (String_ContainsConst(&renderer, "Intel")) { + Chat_AddRaw("&cIntel graphics cards are known to have issues with the OpenGL build."); + Chat_AddRaw("&cVSync may not work, and you may see disappearing clouds and map edges."); + #ifdef CC_BUILD_WIN + Chat_AddRaw("&cTry downloading the Direct3D 9 build instead."); + #endif + return true; + } + return false; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Compatibility-----------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_GL11 +static void GLBackend_Init(void) { MakeIndices(gl_indices, GFX_MAX_INDICES, NULL); } +#else + +#ifdef CC_BUILD_GL11_FALLBACK +static FP_glDrawElements _realDrawElements; +static FP_glColorPointer _realColorPointer; +static FP_glTexCoordPointer _realTexCoordPointer; +static FP_glVertexPointer _realVertexPointer; + +/* On Windows, can replace the GL function drawing with these 1.1 fallbacks */ +/* fake vertex buffer objects by using client side pointers instead */ +typedef struct legacy_buffer { cc_uint8* data; } legacy_buffer; +static legacy_buffer* cur_ib; +static legacy_buffer* cur_vb; +#define legacy_GetBuffer(target) (target == GL_ELEMENT_ARRAY_BUFFER ? &cur_ib : &cur_vb); + +static void APIENTRY legacy_genBuffer(GLsizei n, GLuint* buffer) { + GfxResourceID* dst = (GfxResourceID*)buffer; + *dst = Mem_TryAllocCleared(1, sizeof(legacy_buffer)); +} + +static void APIENTRY legacy_deleteBuffer(GLsizei n, const GLuint* buffer) { + GfxResourceID* dst = (GfxResourceID*)buffer; + Mem_Free(*dst); +} + +static void APIENTRY legacy_bindBuffer(GLenum target, GfxResourceID src) { + legacy_buffer** buffer = legacy_GetBuffer(target); + *buffer = (legacy_buffer*)src; +} + +static void APIENTRY legacy_bufferData(GLenum target, cc_uintptr size, const GLvoid* data, GLenum usage) { + legacy_buffer* buffer = *legacy_GetBuffer(target); + Mem_Free(buffer->data); + + buffer->data = Mem_TryAlloc(size, 1); + if (data) Mem_Copy(buffer->data, data, size); +} + +static void APIENTRY legacy_bufferSubData(GLenum target, cc_uintptr offset, cc_uintptr size, const GLvoid* data) { + legacy_buffer* buffer = *legacy_GetBuffer(target); + Mem_Copy(buffer->data, data, size); +} + + +static void APIENTRY gl10_bindTexture(GLenum target, GLuint texture) { + +} +static void APIENTRY gl10_deleteTexture(GLsizei n, const GLuint* textures) { + +} +static void APIENTRY gl10_genTexture(GLsizei n, GLuint* textures) { + +} +static void APIENTRY gl10_texImage(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels) { + +} +static void APIENTRY gl10_texSubImage(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid* pixels) { + +} + +static cc_uint8* gl10_vb; +static void APIENTRY gl10_drawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices) { + /* TODO */ + int i; + glBegin(GL_QUADS); + count = (count * 4) / 6; + + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + struct VertexTextured* src = (struct VertexTextured*)gl10_vb; + for (i = 0; i < count; i++, src++) + { + glColor4ub(PackedCol_R(src->Col), PackedCol_G(src->Col), PackedCol_B(src->Col), PackedCol_A(src->Col)); + glTexCoord2f(src->U, src->V); + glVertex3f(src->x, src->y, src->z); + } + } else { + struct VertexColoured* src = (struct VertexColoured*)gl10_vb; + for (i = 0; i < count; i++, src++) + { + glColor4ub(PackedCol_R(src->Col), PackedCol_G(src->Col), PackedCol_B(src->Col), PackedCol_A(src->Col)); + glVertex3f(src->x, src->y, src->z); + } + } + + glEnd(); +} +static void APIENTRY gl10_colorPointer(GLint size, GLenum type, GLsizei stride, GLpointer offset) { +} +static void APIENTRY gl10_texCoordPointer(GLint size, GLenum type, GLsizei stride, GLpointer offset) { +} +static void APIENTRY gl10_vertexPointer(GLint size, GLenum type, GLsizei stride, GLpointer offset) { + gl10_vb = cur_vb->data + offset; +} + + +static void APIENTRY gl11_drawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices) { + _realDrawElements(mode, count, type, (cc_uintptr)indices + cur_ib->data); +} +static void APIENTRY gl11_colorPointer(GLint size, GLenum type, GLsizei stride, GLpointer offset) { + _realColorPointer(size, type, stride, (cc_uintptr)cur_vb->data + offset); +} +static void APIENTRY gl11_texCoordPointer(GLint size, GLenum type, GLsizei stride, GLpointer offset) { + _realTexCoordPointer(size, type, stride, (cc_uintptr)cur_vb->data + offset); +} +static void APIENTRY gl11_vertexPointer(GLint size, GLenum type, GLsizei stride, GLpointer offset) { + _realVertexPointer(size, type, stride, (cc_uintptr)cur_vb->data + offset); +} + + +static void FallbackOpenGL(void) { + Window_ShowDialog("Performance warning", + "Your system only supports only OpenGL 1.1\n" \ + "This is usually caused by graphics drivers not being installed\n\n" \ + "As such you will likely experience very poor performance"); + customMipmapsLevels = false; + + _glGenBuffers = legacy_genBuffer; + _glDeleteBuffers = legacy_deleteBuffer; + _glBindBuffer = legacy_bindBuffer; + _glBufferData = legacy_bufferData; + _glBufferSubData = legacy_bufferSubData; + + _realDrawElements = _glDrawElements; _realColorPointer = _glColorPointer; + _realTexCoordPointer = _glTexCoordPointer; _realVertexPointer = _glVertexPointer; + + _glDrawElements = gl11_drawElements; _glColorPointer = gl11_colorPointer; + _glTexCoordPointer = gl11_texCoordPointer; _glVertexPointer = gl11_vertexPointer; + + /* OpenGL 1.0 fallback support */ + if (_realDrawElements) return; + Window_ShowDialog("Performance warning", "OpenGL 1.0 only support, expect awful performance"); + + _glDrawElements = gl10_drawElements; _glColorPointer = gl10_colorPointer; + _glTexCoordPointer = gl10_texCoordPointer; _glVertexPointer = gl10_vertexPointer; + + _glBindTexture = gl10_bindTexture; + _glGenTextures = gl10_genTexture; + _glDeleteTextures = gl10_deleteTexture; + _glTexImage2D = gl10_texImage; + _glTexSubImage2D = gl10_texSubImage; +} +#else +/* No point in even trying for other systems */ +static void FallbackOpenGL(void) { + Logger_FailToStart("Only OpenGL 1.1 supported.\n\n" \ + "Compile the game with CC_BUILD_GL11, or ask on the ClassiCube forums for it"); +} +#endif + +static void GLBackend_Init(void) { + static const struct DynamicLibSym coreVboFuncs[] = { + DynamicLib_Sym2("glBindBuffer", glBindBuffer), DynamicLib_Sym2("glDeleteBuffers", glDeleteBuffers), + DynamicLib_Sym2("glGenBuffers", glGenBuffers), DynamicLib_Sym2("glBufferData", glBufferData), + DynamicLib_Sym2("glBufferSubData", glBufferSubData) + }; + static const struct DynamicLibSym arbVboFuncs[] = { + DynamicLib_Sym2("glBindBufferARB", glBindBuffer), DynamicLib_Sym2("glDeleteBuffersARB", glDeleteBuffers), + DynamicLib_Sym2("glGenBuffersARB", glGenBuffers), DynamicLib_Sym2("glBufferDataARB", glBufferData), + DynamicLib_Sym2("glBufferSubDataARB", glBufferSubData) + }; + static const cc_string vboExt = String_FromConst("GL_ARB_vertex_buffer_object"); + cc_string extensions = String_FromReadonly((const char*)glGetString(GL_EXTENSIONS)); + const GLubyte* ver = glGetString(GL_VERSION); + + /* Version string is always: x.y. (and whatever afterwards) */ + int major = ver[0] - '0', minor = ver[2] - '0'; +#ifdef CC_BUILD_GL11_FALLBACK + LoadCoreFuncs(); +#endif + customMipmapsLevels = true; + + /* Supported in core since 1.5 */ + if (major > 1 || (major == 1 && minor >= 5)) { + GLContext_GetAll(coreVboFuncs, Array_Elems(coreVboFuncs)); + } else if (String_CaselessContains(&extensions, &vboExt)) { + GLContext_GetAll(arbVboFuncs, Array_Elems(arbVboFuncs)); + } else { + FallbackOpenGL(); + } +} +#endif +#endif diff --git a/src/Graphics_GL2.c b/src/Graphics_GL2.c new file mode 100644 index 0000000..d9d6a8d --- /dev/null +++ b/src/Graphics_GL2.c @@ -0,0 +1,661 @@ +/* Silence deprecation warnings on modern macOS/iOS */ +#define GL_SILENCE_DEPRECATION +#define GLES_SILENCE_DEPRECATION + +#include "Core.h" +#if CC_GFX_BACKEND == CC_GFX_BACKEND_GL2 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" + +/* OpenGL 2.0 backend (alternative modern-ish backend) */ +#include "../misc/opengl/GLCommon.h" + +/* e.g. GLAPI void APIENTRY glFunction(int args); */ +#define GL_FUNC(_retType, name) GLAPI _retType APIENTRY name +#include "../misc/opengl/GL1Funcs.h" + +/* Functions must be dynamically linked on Windows */ +#ifdef CC_BUILD_WIN +/* e.g. static void (APIENTRY *_glFunction)(int args); */ +#undef GL_FUNC +#define GL_FUNC(_retType, name) static _retType (APIENTRY *name) +#include "../misc/opengl/GL2Funcs.h" + +#define GLSym(sym) { DYNAMICLIB_QUOTE(sym), (void**)& ## sym } +static const struct DynamicLibSym core_funcs[] = { + GLSym(glBindBuffer), GLSym(glDeleteBuffers), GLSym(glGenBuffers), GLSym(glBufferData), GLSym(glBufferSubData), + + GLSym(glCreateShader), GLSym(glDeleteShader), GLSym(glGetShaderiv), GLSym(glGetShaderInfoLog), GLSym(glShaderSource), + GLSym(glAttachShader), GLSym(glBindAttribLocation), GLSym(glCompileShader), GLSym(glDetachShader), GLSym(glLinkProgram), + + GLSym(glCreateProgram), GLSym(glDeleteProgram), GLSym(glGetProgramiv), GLSym(glGetProgramInfoLog), GLSym(glUseProgram), + + GLSym(glDisableVertexAttribArray), GLSym(glEnableVertexAttribArray), GLSym(glVertexAttribPointer), + + GLSym(glGetUniformLocation), GLSym(glUniform1f), GLSym(glUniform2f), GLSym(glUniform3f), GLSym(glUniformMatrix4fv), +}; +#else +#include "../misc/opengl/GL2Funcs.h" +#endif + +#define _glBindTexture glBindTexture +#define _glDeleteTextures glDeleteTextures +#define _glGenTextures glGenTextures +#define _glTexImage2D glTexImage2D +#define _glTexSubImage2D glTexSubImage2D + +#include "_GLShared.h" +static GfxResourceID white_square; + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static GLuint GL_GenAndBind(GLenum target) { + GLuint id; + glGenBuffers(1, &id); + glBindBuffer(target, id); + return id; +} + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + cc_uint16 indices[GFX_MAX_INDICES]; + GLuint id = GL_GenAndBind(GL_ELEMENT_ARRAY_BUFFER); + cc_uint32 size = count * sizeof(cc_uint16); + + fillFunc(indices, count, obj); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, indices, GL_STATIC_DRAW); + return uint_to_ptr(id); +} + +void Gfx_BindIb(GfxResourceID ib) { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ptr_to_uint(ib)); +} + +void Gfx_DeleteIb(GfxResourceID* ib) { + GLuint id = ptr_to_uint(*ib); + if (!id) return; + glDeleteBuffers(1, &id); + *ib = 0; +} + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + GLuint id = GL_GenAndBind(GL_ARRAY_BUFFER); + return uint_to_ptr(id); +} + +void Gfx_BindVb(GfxResourceID vb) { + glBindBuffer(GL_ARRAY_BUFFER, ptr_to_uint(vb)); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { + GLuint id = ptr_to_uint(*vb); + if (id) glDeleteBuffers(1, &id); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return FastAllocTempMem(count * strideSizes[fmt]); +} + +void Gfx_UnlockVb(GfxResourceID vb) { + glBufferData(GL_ARRAY_BUFFER, tmpSize, tmpData, GL_STATIC_DRAW); +} + + +/*########################################################################################################################* +*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + GLuint id = GL_GenAndBind(GL_ARRAY_BUFFER); + cc_uint32 size = maxVertices * strideSizes[fmt]; + + glBufferData(GL_ARRAY_BUFFER, size, NULL, GL_DYNAMIC_DRAW); + return uint_to_ptr(id); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { + glBindBuffer(GL_ARRAY_BUFFER, ptr_to_uint(vb)); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { + GLuint id = ptr_to_uint(*vb); + if (id) glDeleteBuffers(1, &id); + *vb = 0; +} + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return FastAllocTempMem(count * strideSizes[fmt]); +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + glBindBuffer(GL_ARRAY_BUFFER, ptr_to_uint(vb)); + glBufferSubData(GL_ARRAY_BUFFER, 0, tmpSize, tmpData); +} + +void Gfx_SetDynamicVbData(GfxResourceID vb, void* vertices, int vCount) { + cc_uint32 size = vCount * gfx_stride; + glBindBuffer(GL_ARRAY_BUFFER, ptr_to_uint(vb)); + glBufferSubData(GL_ARRAY_BUFFER, 0, size, vertices); +} + + +/*########################################################################################################################* +*------------------------------------------------------OpenGL modern------------------------------------------------------* +*#########################################################################################################################*/ +#define FTR_TEXTURE_UV (1 << 0) +#define FTR_ALPHA_TEST (1 << 1) +#define FTR_TEX_OFFSET (1 << 2) +#define FTR_LINEAR_FOG (1 << 3) +#define FTR_DENSIT_FOG (1 << 4) +#define FTR_HASANY_FOG (FTR_LINEAR_FOG | FTR_DENSIT_FOG) +#define FTR_FS_MEDIUMP (1 << 7) + +#define UNI_MVP_MATRIX (1 << 0) +#define UNI_TEX_OFFSET (1 << 1) +#define UNI_FOG_COL (1 << 2) +#define UNI_FOG_END (1 << 3) +#define UNI_FOG_DENS (1 << 4) +#define UNI_MASK_ALL 0x1F + +/* cached uniforms (cached for multiple programs */ +static struct Matrix _view, _proj, _mvp; +static cc_bool gfx_texTransform; +static float _texX, _texY; +static PackedCol gfx_fogColor; +static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f; +static int gfx_fogMode = -1; + +/* shader programs (emulate fixed function) */ +static struct GLShader { + int features; /* what features are enabled for this shader */ + int uniforms; /* which associated uniforms need to be resent to GPU */ + GLuint program; /* OpenGL program ID (0 if not yet compiled) */ + int locations[5]; /* location of uniforms (not constant) */ +} shaders[6 * 3] = { + /* no fog */ + { 0 }, + { 0 | FTR_ALPHA_TEST }, + { FTR_TEXTURE_UV }, + { FTR_TEXTURE_UV | FTR_ALPHA_TEST }, + { FTR_TEXTURE_UV | FTR_TEX_OFFSET }, + { FTR_TEXTURE_UV | FTR_TEX_OFFSET | FTR_ALPHA_TEST }, + /* linear fog */ + { FTR_LINEAR_FOG | 0 }, + { FTR_LINEAR_FOG | 0 | FTR_ALPHA_TEST }, + { FTR_LINEAR_FOG | FTR_TEXTURE_UV }, + { FTR_LINEAR_FOG | FTR_TEXTURE_UV | FTR_ALPHA_TEST }, + { FTR_LINEAR_FOG | FTR_TEXTURE_UV | FTR_TEX_OFFSET }, + { FTR_LINEAR_FOG | FTR_TEXTURE_UV | FTR_TEX_OFFSET | FTR_ALPHA_TEST }, + /* density fog */ + { FTR_DENSIT_FOG | 0 }, + { FTR_DENSIT_FOG | 0 | FTR_ALPHA_TEST }, + { FTR_DENSIT_FOG | FTR_TEXTURE_UV }, + { FTR_DENSIT_FOG | FTR_TEXTURE_UV | FTR_ALPHA_TEST }, + { FTR_DENSIT_FOG | FTR_TEXTURE_UV | FTR_TEX_OFFSET }, + { FTR_DENSIT_FOG | FTR_TEXTURE_UV | FTR_TEX_OFFSET | FTR_ALPHA_TEST }, +}; +static struct GLShader* gfx_activeShader; + +/* Generates source code for a GLSL vertex shader, based on shader's flags */ +static void GenVertexShader(const struct GLShader* shader, cc_string* dst) { + int uv = shader->features & FTR_TEXTURE_UV; + int tm = shader->features & FTR_TEX_OFFSET; + + String_AppendConst(dst, "attribute vec3 in_pos;\n"); + String_AppendConst(dst, "attribute vec4 in_col;\n"); + if (uv) String_AppendConst(dst, "attribute vec2 in_uv;\n"); + String_AppendConst(dst, "varying vec4 out_col;\n"); + if (uv) String_AppendConst(dst, "varying vec2 out_uv;\n"); + String_AppendConst(dst, "uniform mat4 mvp;\n"); + if (tm) String_AppendConst(dst, "uniform vec2 texOffset;\n"); + + String_AppendConst(dst, "void main() {\n"); + String_AppendConst(dst, " gl_Position = mvp * vec4(in_pos, 1.0);\n"); + String_AppendConst(dst, " out_col = in_col;\n"); + if (uv) String_AppendConst(dst, " out_uv = in_uv;\n"); + if (tm) String_AppendConst(dst, " out_uv = out_uv + texOffset;\n"); + String_AppendConst(dst, "}"); +} + +/* Generates source code for a GLSL fragment shader, based on shader's flags */ +static void GenFragmentShader(const struct GLShader* shader, cc_string* dst) { + int uv = shader->features & FTR_TEXTURE_UV; + int al = shader->features & FTR_ALPHA_TEST; + int fl = shader->features & FTR_LINEAR_FOG; + int fd = shader->features & FTR_DENSIT_FOG; + int fm = shader->features & FTR_HASANY_FOG; + +#ifdef CC_BUILD_GLES + int mp = shader->features & FTR_FS_MEDIUMP; + if (mp) String_AppendConst(dst, "precision mediump float;\n"); + else String_AppendConst(dst, "precision highp float;\n"); +#endif + + String_AppendConst(dst, "varying vec4 out_col;\n"); + if (uv) String_AppendConst(dst, "varying vec2 out_uv;\n"); + if (uv) String_AppendConst(dst, "uniform sampler2D texImage;\n"); + if (fm) String_AppendConst(dst, "uniform vec3 fogCol;\n"); + if (fl) String_AppendConst(dst, "uniform float fogEnd;\n"); + if (fd) String_AppendConst(dst, "uniform float fogDensity;\n"); + + String_AppendConst(dst, "void main() {\n"); + if (uv) String_AppendConst(dst, " vec4 col = texture2D(texImage, out_uv) * out_col;\n"); + else String_AppendConst(dst, " vec4 col = out_col;\n"); + if (al) String_AppendConst(dst, " if (col.a < 0.5) discard;\n"); + if (fm) String_AppendConst(dst, " float depth = 1.0 / gl_FragCoord.w;\n"); + if (fl) String_AppendConst(dst, " float f = clamp((fogEnd - depth) / fogEnd, 0.0, 1.0);\n"); + if (fd) String_AppendConst(dst, " float f = clamp(exp(fogDensity * depth), 0.0, 1.0);\n"); + if (fm) String_AppendConst(dst, " col.rgb = mix(fogCol, col.rgb, f);\n"); + String_AppendConst(dst, " gl_FragColor = col;\n"); + String_AppendConst(dst, "}"); +} + +/* Tries to compile GLSL shader code */ +static GLint CompileShader(GLint shader, const cc_string* src) { + const char* str = src->buffer; + int len = src->length; + GLint temp; + + glShaderSource(shader, 1, &str, &len); + glCompileShader(shader); + glGetShaderiv(shader, GL_COMPILE_STATUS, &temp); + return temp; +} + +/* Logs information then aborts program */ +static void ShaderFailed(GLint shader) { + char logInfo[2048]; + GLint temp; + if (!shader) Logger_Abort("Failed to create shader"); + + temp = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &temp); + + if (temp > 1) { + glGetShaderInfoLog(shader, 2047, NULL, logInfo); + logInfo[2047] = '\0'; + Window_ShowDialog("Failed to compile shader", logInfo); + } + Logger_Abort("Failed to compile shader"); +} + +/* Tries to compile vertex and fragment shaders, then link into an OpenGL program */ +static void CompileProgram(struct GLShader* shader) { + char tmpBuffer[2048]; cc_string tmp; + GLuint vs, fs, program; + GLint temp; + + vs = glCreateShader(GL_VERTEX_SHADER); + if (!vs) { Platform_LogConst("Failed to create vertex shader"); return; } + + String_InitArray(tmp, tmpBuffer); + GenVertexShader(shader, &tmp); + if (!CompileShader(vs, &tmp)) ShaderFailed(vs); + + fs = glCreateShader(GL_FRAGMENT_SHADER); + if (!fs) { Platform_LogConst("Failed to create fragment shader"); glDeleteShader(vs); return; } + + tmp.length = 0; + GenFragmentShader(shader, &tmp); + if (!CompileShader(fs, &tmp)) { + /* Sometimes fails 'highp precision is not supported in fragment shader' */ + /* So try compiling shader again without highp precision */ + shader->features |= FTR_FS_MEDIUMP; + + tmp.length = 0; + GenFragmentShader(shader, &tmp); + if (!CompileShader(fs, &tmp)) ShaderFailed(fs); + } + + + program = glCreateProgram(); + if (!program) Logger_Abort("Failed to create program"); + shader->program = program; + + glAttachShader(program, vs); + glAttachShader(program, fs); + + /* Force in_pos/in_col/in_uv attributes to be bound to 0,1,2 locations */ + /* Although most browsers assign the attributes in this order anyways, */ + /* the specification does not require this. (e.g. Safari doesn't) */ + glBindAttribLocation(program, 0, "in_pos"); + glBindAttribLocation(program, 1, "in_col"); + glBindAttribLocation(program, 2, "in_uv"); + + glLinkProgram(program); + glGetProgramiv(program, GL_LINK_STATUS, &temp); + + if (temp) { + glDetachShader(program, vs); + glDetachShader(program, fs); + + glDeleteShader(vs); + glDeleteShader(fs); + + shader->locations[0] = glGetUniformLocation(program, "mvp"); + shader->locations[1] = glGetUniformLocation(program, "texOffset"); + shader->locations[2] = glGetUniformLocation(program, "fogCol"); + shader->locations[3] = glGetUniformLocation(program, "fogEnd"); + shader->locations[4] = glGetUniformLocation(program, "fogDensity"); + return; + } + temp = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &temp); + + if (temp > 0) { + glGetProgramInfoLog(program, 2047, NULL, tmpBuffer); + tmpBuffer[2047] = '\0'; + Window_ShowDialog("Failed to compile program", tmpBuffer); + } + Logger_Abort("Failed to compile program"); +} + +/* Marks a uniform as changed on all programs */ +static void DirtyUniform(int uniform) { + int i; + for (i = 0; i < Array_Elems(shaders); i++) { + shaders[i].uniforms |= uniform; + } +} + +/* Sends changed uniforms to the GPU for current program */ +static void ReloadUniforms(void) { + struct GLShader* s = gfx_activeShader; + if (!s) return; /* NULL if context is lost */ + + if (s->uniforms & UNI_MVP_MATRIX) { + glUniformMatrix4fv(s->locations[0], 1, false, (float*)&_mvp); + s->uniforms &= ~UNI_MVP_MATRIX; + } + if ((s->uniforms & UNI_TEX_OFFSET) && (s->features & FTR_TEX_OFFSET)) { + glUniform2f(s->locations[1], _texX, _texY); + s->uniforms &= ~UNI_TEX_OFFSET; + } + if ((s->uniforms & UNI_FOG_COL) && (s->features & FTR_HASANY_FOG)) { + glUniform3f(s->locations[2], PackedCol_R(gfx_fogColor) / 255.0f, PackedCol_G(gfx_fogColor) / 255.0f, + PackedCol_B(gfx_fogColor) / 255.0f); + s->uniforms &= ~UNI_FOG_COL; + } + if ((s->uniforms & UNI_FOG_END) && (s->features & FTR_LINEAR_FOG)) { + glUniform1f(s->locations[3], gfx_fogEnd); + s->uniforms &= ~UNI_FOG_END; + } + if ((s->uniforms & UNI_FOG_DENS) && (s->features & FTR_DENSIT_FOG)) { + /* See https://docs.microsoft.com/en-us/previous-versions/ms537113(v%3Dvs.85) */ + /* The equation for EXP mode is exp(-density * z), so just negate density here */ + glUniform1f(s->locations[4], -gfx_fogDensity); + s->uniforms &= ~UNI_FOG_DENS; + } +} + +/* Switches program to one that duplicates current fixed function state */ +/* Compiles program and reloads uniforms if needed */ +static void SwitchProgram(void) { + struct GLShader* shader; + int index = 0; + + if (gfx_fogEnabled) { + index += 6; /* linear fog */ + if (gfx_fogMode >= 1) index += 6; /* exp fog */ + } + + if (gfx_format == VERTEX_FORMAT_TEXTURED) index += 2; + if (gfx_texTransform) index += 2; + if (gfx_alphaTest) index += 1; + + shader = &shaders[index]; + if (shader == gfx_activeShader) { ReloadUniforms(); return; } + if (!shader->program) CompileProgram(shader); + + gfx_activeShader = shader; + glUseProgram(shader->program); + ReloadUniforms(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_BindTexture(GfxResourceID texId) { + /* Texture 0 has different behaviour depending on backend */ + /* Desktop OpenGL - pure white 1x1 texture */ + /* WebGL/OpenGL ES - pure black 1x1 texture */ + /* So for consistency, always use a 1x1 pure white texture */ + if (!texId) texId = white_square; + glBindTexture(GL_TEXTURE_2D, ptr_to_uint(texId)); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) { gfx_fogEnabled = enabled; SwitchProgram(); } +void Gfx_SetFogCol(PackedCol color) { + if (color == gfx_fogColor) return; + gfx_fogColor = color; + DirtyUniform(UNI_FOG_COL); + ReloadUniforms(); +} + +void Gfx_SetFogDensity(float value) { + if (gfx_fogDensity == value) return; + gfx_fogDensity = value; + DirtyUniform(UNI_FOG_DENS); + ReloadUniforms(); +} + +void Gfx_SetFogEnd(float value) { + if (gfx_fogEnd == value) return; + gfx_fogEnd = value; + DirtyUniform(UNI_FOG_END); + ReloadUniforms(); +} + +void Gfx_SetFogMode(FogFunc func) { + if (gfx_fogMode == func) return; + gfx_fogMode = func; + SwitchProgram(); +} + +static void SetAlphaTest(cc_bool enabled) { SwitchProgram(); } + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + Matrix_Mul(&_mvp, &_view, &_proj); + DirtyUniform(UNI_MVP_MATRIX); + ReloadUniforms(); +} +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + _texX = x; _texY = y; + gfx_texTransform = true; + DirtyUniform(UNI_TEX_OFFSET); + SwitchProgram(); +} + +void Gfx_DisableTextureOffset(void) { + gfx_texTransform = false; + SwitchProgram(); +} + + +/*########################################################################################################################* +*-------------------------------------------------------State setup-------------------------------------------------------* +*#########################################################################################################################*/ +static void GLContext_GetAll(const struct DynamicLibSym* syms, int count) { + int i; + for (i = 0; i < count; i++) + { + *syms[i].symAddr = GLContext_GetAddress(syms[i].name); + } +} + +static void GLBackend_Init(void) { +#ifdef CC_BUILD_WIN + GLContext_GetAll(core_funcs, Array_Elems(core_funcs)); +#endif + +#ifdef CC_BUILD_GLES + // OpenGL ES 2.0 doesn't support custom mipmaps levels, but 3.2 does + // Note that GL_MAJOR_VERSION and GL_MINOR_VERSION were not actually + // implemented until 3.0.. but hopefully older GPU drivers out there + // don't try and set a value even when it's unsupported + #define _GL_MAJOR_VERSION 33307 + #define _GL_MINOR_VERSION 33308 + + GLint major = 0, minor = 0; + glGetIntegerv(_GL_MAJOR_VERSION, &major); + glGetIntegerv(_GL_MINOR_VERSION, &minor); + customMipmapsLevels = major >= 3 && minor >= 2; +#else + customMipmapsLevels = true; + const GLubyte* ver = glGetString(GL_VERSION); + int major = ver[0] - '0', minor = ver[2] - '0'; + if (major >= 2) return; + + // OpenGL 1.x.. will likely either not work or perform poorly + cc_string str; char strBuffer[1024]; + String_InitArray_NT(str, strBuffer); + String_Format2(&str,"Modern OpenGL build requires at least OpenGL 2.0\n" \ + "Your system only supports OpenGL %i.%i however\n\n" \ + "As such ClassiCube will likely perform poorly or not work\n" \ + "It is recommended you use the Normal OpenGL build instead\n", + &major, &minor); + strBuffer[str.length] = '\0'; + Window_ShowDialog("Compatibility warning", strBuffer); +#endif +} + +static void Gfx_FreeState(void) { + int i; + FreeDefaultResources(); + gfx_activeShader = NULL; + + for (i = 0; i < Array_Elems(shaders); i++) { + glDeleteProgram(shaders[i].program); + shaders[i].program = 0; + } + Gfx_DeleteTexture(&white_square); +} + +static void Gfx_RestoreState(void) { + InitDefaultResources(); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + gfx_format = -1; + + DirtyUniform(UNI_MASK_ALL); + GL_ClearColor(gfx_clearColor); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthFunc(GL_LEQUAL); + + /* 1x1 dummy white texture */ + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + Gfx_RecreateTexture(&white_square, &bmp, 0, false); +} +cc_bool Gfx_WarnIfNecessary(void) { return false; } + + +/*########################################################################################################################* +*----------------------------------------------------------Drawing--------------------------------------------------------* +*#########################################################################################################################*/ +typedef void (*GL_SetupVBFunc)(void); +typedef void (*GL_SetupVBRangeFunc)(int startVertex); +static GL_SetupVBFunc gfx_setupVBFunc; +static GL_SetupVBRangeFunc gfx_setupVBRangeFunc; + +static void GL_SetupVbColoured(void) { + glVertexAttribPointer(0, 3, GL_FLOAT, false, SIZEOF_VERTEX_COLOURED, uint_to_ptr( 0)); + glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, true, SIZEOF_VERTEX_COLOURED, uint_to_ptr(12)); +} + +static void GL_SetupVbTextured(void) { + glVertexAttribPointer(0, 3, GL_FLOAT, false, SIZEOF_VERTEX_TEXTURED, uint_to_ptr( 0)); + glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, true, SIZEOF_VERTEX_TEXTURED, uint_to_ptr(12)); + glVertexAttribPointer(2, 2, GL_FLOAT, false, SIZEOF_VERTEX_TEXTURED, uint_to_ptr(16)); +} + +static void GL_SetupVbColoured_Range(int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_COLOURED; + glVertexAttribPointer(0, 3, GL_FLOAT, false, SIZEOF_VERTEX_COLOURED, uint_to_ptr(offset )); + glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, true, SIZEOF_VERTEX_COLOURED, uint_to_ptr(offset + 12)); +} + +static void GL_SetupVbTextured_Range(int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + glVertexAttribPointer(0, 3, GL_FLOAT, false, SIZEOF_VERTEX_TEXTURED, uint_to_ptr(offset )); + glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, true, SIZEOF_VERTEX_TEXTURED, uint_to_ptr(offset + 12)); + glVertexAttribPointer(2, 2, GL_FLOAT, false, SIZEOF_VERTEX_TEXTURED, uint_to_ptr(offset + 16)); +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + glEnableVertexAttribArray(2); + gfx_setupVBFunc = GL_SetupVbTextured; + gfx_setupVBRangeFunc = GL_SetupVbTextured_Range; + } else { + glDisableVertexAttribArray(2); + gfx_setupVBFunc = GL_SetupVbColoured; + gfx_setupVBRangeFunc = GL_SetupVbColoured_Range; + } + SwitchProgram(); +} + +void Gfx_DrawVb_Lines(int verticesCount) { + gfx_setupVBFunc(); + glDrawArrays(GL_LINES, 0, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + gfx_setupVBRangeFunc(startVertex); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, NULL); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + gfx_setupVBFunc(); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, NULL); +} + +void Gfx_BindVb_Textured(GfxResourceID vb) { + Gfx_BindVb(vb); + GL_SetupVbTextured(); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + if (startVertex + verticesCount > GFX_MAX_VERTICES) { + GL_SetupVbTextured_Range(startVertex); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, NULL); + GL_SetupVbTextured(); + } else { + /* ICOUNT(startVertex) * 2 = startVertex * 3 */ + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, (void*)(startVertex * 3)); + } +} +#endif diff --git a/src/Graphics_N64.c b/src/Graphics_N64.c new file mode 100644 index 0000000..23c1911 --- /dev/null +++ b/src/Graphics_N64.c @@ -0,0 +1,491 @@ +#include "Core.h" +#ifdef CC_BUILD_N64 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include +#include +#include + +typedef void (*GL_SetupVBFunc)(void); +typedef void (*GL_SetupVBRangeFunc)(int startVertex); +static GL_SetupVBFunc gfx_setupVBFunc; +static GL_SetupVBRangeFunc gfx_setupVBRangeFunc; + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static void GL_UpdateVsync(void) { + //GLContext_SetFpsLimit(gfx_vsync, gfx_minFrameMs); +} +static surface_t zbuffer; + +void Gfx_Create(void) { + gl_init(); + //rdpq_debug_start(); // TODO debug + //rdpq_debug_log(true); + zbuffer = surface_alloc(FMT_RGBA16, display_get_width(), display_get_height()); + + Gfx.MaxTexWidth = 256; + Gfx.MaxTexHeight = 256; + Gfx.Created = true; + + // TMEM only has 4 KB in it, which can be interpreted as + // - 1024 32bpp pixels + // - 2048 16bpp pixels + Gfx.MaxTexSize = 1024; + Gfx.MaxLowResTexSize = 2048; + + Gfx.SupportsNonPowTwoTextures = true; + Gfx_RestoreState(); + GL_UpdateVsync(); +} + +cc_bool Gfx_TryRestoreContext(void) { + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); + gl_close(); +} + +#define gl_Toggle(cap) if (enabled) { glEnable(cap); } else { glDisable(cap); } + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +static color_t gfx_clearColor; + +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using Nintendo 64 --\n"); + String_AppendConst(info, "GPU: Nintendo 64 RDP (LibDragon OpenGL)\n"); + String_AppendConst(info, "T&L: Nintendo 64 RSP (LibDragon OpenGL)\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; + if (Gfx.Created) GL_UpdateVsync(); +} + +void Gfx_OnWindowResize(void) { } + +void Gfx_SetViewport(int x, int y, int w, int h) { } + + +void Gfx_BeginFrame(void) { + surface_t* disp = display_get(); + rdpq_attach(disp, &zbuffer); + + gl_context_begin(); + Platform_LogConst("GFX ctx beg"); +} + +extern void __rdpq_autosync_change(int mode); +static color_t gfx_clearColor; + +void Gfx_ClearBuffers(GfxBuffers buffers) { + __rdpq_autosync_change(AUTOSYNC_PIPE); + + if (buffers & GFX_BUFFER_COLOR) + rdpq_clear(gfx_clearColor); + if (buffers & GFX_BUFFER_DEPTH) + rdpq_clear_z(0xFFFC); +} + +void Gfx_ClearColor(PackedCol color) { + gfx_clearColor.r = PackedCol_R(color); + gfx_clearColor.g = PackedCol_G(color); + gfx_clearColor.b = PackedCol_B(color); + gfx_clearColor.a = PackedCol_A(color); + //memcpy(&gfx_clearColor, &color, sizeof(color_t)); // TODO maybe use proper conversion from graphics.h ?? +} + +void Gfx_EndFrame(void) { + Platform_LogConst("GFX ctx end"); + gl_context_end(); + rdpq_detach_show(); + + if (gfx_minFrameMs) LimitFPS(); +//Platform_LogConst("GFX END"); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCTexture { + surface_t surface; + GLuint textureID; +} CCTexture; + +#define ALIGNUP8(size) (((size) + 7) & ~0x07) + +// A8 B8 G8 R8 > A1 B5 G5 B5 +#define To16BitPixel(src) \ + ((src & 0x80) >> 7) | ((src & 0xF800) >> 10) | ((src & 0xF80000) >> 13) | ((src & 0xF8000000) >> 16); + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + cc_bool bit16 = flags & TEXTURE_FLAG_LOWRES; + // rows are actually 8 byte aligned in TMEM https://github.com/DragonMinded/libdragon/blob/f360fa1bb1fb3ff3d98f4ab58692d40c828636c9/src/rdpq/rdpq_tex.c#L132 + // so even though width * height * pixel size may fit within 4096 bytes, after adjusting for 8 byte alignment, row pitch * height may exceed 4096 bytes + int pitch = bit16 ? ALIGNUP8(bmp->width * 2) : ALIGNUP8(bmp->width * 4); + if (pitch * bmp->height > 4096) return 0; + + CCTexture* tex = Mem_Alloc(1, sizeof(CCTexture), "texture"); + + glGenTextures(1, &tex->textureID); + glBindTexture(GL_TEXTURE_2D, tex->textureID); + // NOTE: Enabling these fixes textures, but seems to break on cen64 + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmaps ? GL_LINEAR : GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mipmaps ? GL_LINEAR : GL_NEAREST); + + tex->surface = surface_alloc(bit16 ? FMT_RGBA16 : FMT_RGBA32, bmp->width, bmp->height); + surface_t* fb = &tex->surface; + + if (bit16) { + cc_uint32* src = (cc_uint32*)bmp->scan0; + cc_uint8* dst = (cc_uint8*)fb->buffer; + + // 16 bpp requires reducing A8R8G8B8 to A1R5G5B5 + for (int y = 0; y < bmp->height; y++) + { + cc_uint32* src_row = src + y * rowWidth; + cc_uint16* dst_row = (cc_uint16*)(dst + y * fb->stride); + + for (int x = 0; x < bmp->width; x++) + { + dst_row[x] = To16BitPixel(src_row[x]); + } + } + } else { + // 32 bpp can just be copied straight across + CopyTextureData(fb->buffer, fb->stride, bmp, rowWidth << 2); + } + + + rdpq_texparms_t params = + { + .s.repeats = (flags & TEXTURE_FLAG_NONPOW2) ? 1 : REPEAT_INFINITE, + .t.repeats = (flags & TEXTURE_FLAG_NONPOW2) ? 1 : REPEAT_INFINITE, + }; + + // rdpq_tex_upload(TILE0, &tex->surface, ¶ms); + glSurfaceTexImageN64(GL_TEXTURE_2D, 0, fb, ¶ms); + return tex; +} + +void Gfx_BindTexture(GfxResourceID texId) { + CCTexture* tex = (CCTexture*)texId; + GLuint glID = tex ? tex->textureID : 0; + //Platform_Log1("BIND: %i", &glID); + + //rdpq_debug_log(true); + glBindTexture(GL_TEXTURE_2D, glID); + // rdpq_debug_log(false); +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + // TODO: Just memcpying doesn't actually work. maybe due to glSurfaceTexImageN64 caching the RSQ upload block? + // TODO: Is there a more optimised approach than just calling glSurfaceTexImageN64 + CCTexture* tex = (CCTexture*)texId; + + surface_t* fb = &tex->surface; + cc_uint32* src = (cc_uint32*)part->scan0 + x; + cc_uint8* dst = (cc_uint8*)fb->buffer + (x * 4) + (y * fb->stride); + + for (int srcY = 0; srcY < part->height; srcY++) + { + Mem_Copy(dst + srcY * fb->stride, + src + srcY * rowWidth, + part->width * 4); + } + + + glBindTexture(GL_TEXTURE_2D, tex->textureID); + rdpq_texparms_t params = (rdpq_texparms_t){ + .s.repeats = REPEAT_INFINITE, + .t.repeats = REPEAT_INFINITE, + }; + // rdpq_tex_upload(TILE0, &tex->surface, ¶ms); + glSurfaceTexImageN64(GL_TEXTURE_2D, 0, fb, ¶ms); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + CCTexture* tex = (CCTexture*)(*texId); + if (!tex) return; + + glDeleteTextures(1, &tex->textureID); + Mem_Free(tex); + *texId = NULL; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFaceCulling(cc_bool enabled) { gl_Toggle(GL_CULL_FACE); } +static void SetAlphaBlend(cc_bool enabled) { gl_Toggle(GL_BLEND); } +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + //glColorMask(r, g, b, a); TODO +} + +void Gfx_SetDepthWrite(cc_bool enabled) { glDepthMask(enabled); } +void Gfx_SetDepthTest(cc_bool enabled) { gl_Toggle(GL_DEPTH_TEST); } + +static void Gfx_FreeState(void) { FreeDefaultResources(); } +static void Gfx_RestoreState(void) { + InitDefaultResources(); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + gfx_format = -1; + + glHint(GL_FOG_HINT, GL_NICEST); + glAlphaFunc(GL_GREATER, 0.5f); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthFunc(GL_LESS); + //glEnable(GL_RDPQ_TEXTURING_N64); +} + +cc_bool Gfx_WarnIfNecessary(void) { return false; } + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -2.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -(zFar + zNear) / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum */ + /* For a FOV based perspective matrix, left/right/top/bottom are calculated as: */ + /* left = -c * aspect, right = c * aspect, bottom = -c, top = c */ + /* Calculations are simplified because of left/right and top/bottom symmetry */ + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*----------------------------------------------------------Buffers--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint16 __attribute__((aligned(16))) gfx_indices[GFX_MAX_INDICES]; +static void* gfx_vertices; +static int vb_size; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + fillFunc(gfx_indices, count, obj); + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return Mem_TryAlloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool depthOnlyRendering; + +void Gfx_SetFog(cc_bool enabled) { +} + +void Gfx_SetFogCol(PackedCol color) { +} + +void Gfx_SetFogDensity(float value) { +} + +void Gfx_SetFogEnd(float value) { +} + +void Gfx_SetFogMode(FogFunc func) { +} + +static void SetAlphaTest(cc_bool enabled) { + if (enabled) { glEnable(GL_ALPHA_TEST); } else { glDisable(GL_ALPHA_TEST); } +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + depthOnlyRendering = depthOnly; // TODO: Better approach? maybe using glBlendFunc instead? + cc_bool enabled = !depthOnly; + //SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + // enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); + if (enabled) { glEnable(GL_TEXTURE_2D); } else { glDisable(GL_TEXTURE_2D); } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static GLenum matrix_modes[3] = { GL_PROJECTION, GL_MODELVIEW, GL_TEXTURE }; +static int lastMatrix; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadMatrixf((const float*)matrix); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadIdentity(); +} + +static struct Matrix texMatrix = Matrix_IdentityValue; +void Gfx_EnableTextureOffset(float x, float y) { + texMatrix.row4.x = x; texMatrix.row4.y = y; + Gfx_LoadMatrix(2, &texMatrix); +} + +void Gfx_DisableTextureOffset(void) { Gfx_LoadIdentityMatrix(2); } + + +/*########################################################################################################################* +*--------------------------------------------------------Rendering--------------------------------------------------------* +*#########################################################################################################################*/ +#define VB_PTR gfx_vertices +#define IB_PTR gfx_indices + +static void GL_SetupVbColoured(void) { + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_COLOURED, (void*)(VB_PTR + 0)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_COLOURED, (void*)(VB_PTR + 12)); +} + +static void GL_SetupVbTextured(void) { + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + 0)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + 12)); + glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + 16)); +} + +static void GL_SetupVbColoured_Range(int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_COLOURED; + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_COLOURED, (void*)(VB_PTR + offset)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_COLOURED, (void*)(VB_PTR + offset + 12)); +} + +static void GL_SetupVbTextured_Range(int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 12)); + glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 16)); +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnable(GL_TEXTURE_2D); + + gfx_setupVBFunc = GL_SetupVbTextured; + gfx_setupVBRangeFunc = GL_SetupVbTextured_Range; + } else { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisable(GL_TEXTURE_2D); + + gfx_setupVBFunc = GL_SetupVbColoured; + gfx_setupVBRangeFunc = GL_SetupVbColoured_Range; + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { + gfx_setupVBFunc(); + glDrawArrays(GL_LINES, 0, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + gfx_setupVBRangeFunc(startVertex); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, IB_PTR); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + gfx_setupVBFunc(); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, IB_PTR); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + cc_uint32 offset = startVertex * SIZEOF_VERTEX_TEXTURED; + if (depthOnlyRendering) return; + glVertexPointer(3, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset)); + glColorPointer(4, GL_UNSIGNED_BYTE, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 12)); + glTexCoordPointer(2, GL_FLOAT, SIZEOF_VERTEX_TEXTURED, (void*)(VB_PTR + offset + 16)); + glDrawElements(GL_TRIANGLES, ICOUNT(verticesCount), GL_UNSIGNED_SHORT, IB_PTR); +} +#endif diff --git a/src/Graphics_NDS.c b/src/Graphics_NDS.c new file mode 100644 index 0000000..8e7dcf1 --- /dev/null +++ b/src/Graphics_NDS.c @@ -0,0 +1,517 @@ +#include "Core.h" +#ifdef CC_BUILD_NDS +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_Create(void) { + Gfx_RestoreState(); + + Gfx.MinTexWidth = 8; + Gfx.MinTexHeight = 8; + Gfx.MaxTexWidth = 256; + Gfx.MaxTexHeight = 256; + //Gfx.MaxTexSize = 256 * 256; + Gfx.Created = true; + glInit(); + + glClearColor(0, 15, 10, 31); + glClearPolyID(63); + glAlphaFunc(7); + + glClearDepth(GL_MAX_DEPTH); + glViewport(0, 0, 255, 191); + + vramSetBankA(VRAM_A_TEXTURE); + vramSetBankB(VRAM_B_TEXTURE); + vramSetBankC(VRAM_C_TEXTURE); + vramSetBankD(VRAM_D_TEXTURE); + + glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE); +} + +cc_bool Gfx_TryRestoreContext(void) { + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); + vramSetBankA(VRAM_A_LCD); + vramSetBankB(VRAM_B_LCD); + vramSetBankC(VRAM_C_LCD); + vramSetBankD(VRAM_D_LCD); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using Nintendo DS --\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_OnWindowResize(void) { +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } + +void Gfx_BeginFrame(void) { +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + // TODO +} + +void Gfx_ClearColor(PackedCol color) { + int R = PackedCol_R(color) >> 3; + int G = PackedCol_G(color) >> 3; + int B = PackedCol_B(color) >> 3; + glClearColor(R, G, B, 31); +} + +void Gfx_EndFrame(void) { + glFlush(0); + // TODO not needed? + swiWaitForVBlank(); + + if (gfx_minFrameMs) LimitFPS(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +// B8 G8 R8 A8 > R5 G5 B5 A1 +#define BGRA8_to_DS(src) \ + ((src[2] & 0xF8) >> 3) | ((src[1] & 0xF8) << 2) | ((src[0] & 0xF8) << 7) | ((src[3] & 0x80) << 8); + +static void ConvertTexture(cc_uint16* dst, struct Bitmap* bmp, int rowWidth) { + for (int y = 0; y < bmp->height; y++) + { + cc_uint8* src = (cc_uint8*)(bmp->scan0 + y * rowWidth); + + for (int x = 0; x < bmp->width; x++, src += 4) + { + *dst++ = BGRA8_to_DS(src); + } + } +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + vramSetBankA(VRAM_A_TEXTURE); + + cc_uint16* tmp = Mem_TryAlloc(bmp->width * bmp->height, 2); + if (!tmp) return 0; + ConvertTexture(tmp, bmp, rowWidth); + + int textureID; + glGenTextures(1, &textureID); + glBindTexture(0, textureID); + glTexImage2D(0, 0, GL_RGBA, bmp->width, bmp->height, 0, TEXGEN_TEXCOORD, tmp); + glTexParameter(0, GL_TEXTURE_WRAP_S | GL_TEXTURE_WRAP_T); + + cc_uint16* vram_ptr = glGetTexturePointer(textureID); + if (!vram_ptr) Platform_Log2("No VRAM for %i x %i texture", &bmp->width, &bmp->height); + + Mem_Free(tmp); + return (void*)textureID; +} + +void Gfx_BindTexture(GfxResourceID texId) { + glBindTexture(0, (int)texId); +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + int texture = (int)texId; + glBindTexture(0, texture); + + int width = 0; + glGetInt(GL_GET_TEXTURE_WIDTH, &width); + cc_uint16* vram_ptr = glGetTexturePointer(texture); + return; + // TODO doesn't work without VRAM bank changing to LCD and back maybe?? + // (see what glTeximage2D does ??) + + for (int yy = 0; yy < part->height; yy++) + { + cc_uint16* dst = vram_ptr + width * (y + yy) + x; + cc_uint8* src = (cc_uint8*)(part->scan0 + rowWidth * yy); + + for (int xx = 0; xx < part->width; xx++, src += 4, dst++) + { + *dst = BGRA8_to_DS(src); + } + } +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + int texture = (int)(*texId); + if (texture) glDeleteTextures(1, &texture); + *texId = 0; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFaceCulling(cc_bool enabled) { + glPolyFmt(POLY_ALPHA(31) | (enabled ? POLY_CULL_BACK : POLY_CULL_NONE)); +} + +static void SetAlphaBlend(cc_bool enabled) { + /*if (enabled) { + glEnable(GL_BLEND); + } else { + glDisable(GL_BLEND); + }*/ +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_SetDepthWrite(cc_bool enabled) { } +void Gfx_SetDepthTest(cc_bool enabled) { } + +static void Gfx_FreeState(void) { FreeDefaultResources(); } +static void Gfx_RestoreState(void) { + InitDefaultResources(); +} + +cc_bool Gfx_WarnIfNecessary(void) { return true; } + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + *matrix = Matrix_Identity; + width /= 64.0f; + height /= 64.0f; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -2.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -(zFar + zNear) / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum */ + /* For a FOV based perspective matrix, left/right/top/bottom are calculated as: */ + /* left = -c * aspect, right = c * aspect, bottom = -c, top = c */ + /* Calculations are simplified because of left/right and top/bottom symmetry */ + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*----------------------------------------------------------Buffers--------------------------------------------------------* +*#########################################################################################################################*/ +// Preprocess vertex buffers into optimised layout for DS +static VertexFormat buf_fmt; +static int buf_count; + +static void* gfx_vertices; + +struct DSTexturedVertex { + vu32 xy; v16 z; + vu32 rgb; + int u, v; +}; +struct DSColouredVertex { + vu32 xy; v16 z; + vu32 rgb; +}; + +// Precalculate all the expensive vertex data conversion, +// so that actual drawing of them is as fast as possible +static void PreprocessTexturedVertices(void) { + struct VertexTextured* src = gfx_vertices; + struct DSTexturedVertex* dst = gfx_vertices; + + for (int i = 0; i < buf_count; i++, src++, dst++) + { + struct VertexTextured v = *src; + v16 x = floattov16(v.x / 64.0f); + v16 y = floattov16(v.y / 64.0f); + v16 z = floattov16(v.z / 64.0f); + dst->xy = (y << 16) | (x & 0xFFFF); + dst->z = z; + + dst->u = floattof32(v.U); + dst->v = floattof32(v.V); + + int r = PackedCol_R(v.Col); + int g = PackedCol_G(v.Col); + int b = PackedCol_B(v.Col); + dst->rgb = RGB15(r >> 3, g >> 3, b >> 3); + } +} + +static void PreprocessColouredVertices(void) { + struct VertexColoured* src = gfx_vertices; + struct DSColouredVertex* dst = gfx_vertices; + + for (int i = 0; i < buf_count; i++, src++, dst++) + { + struct VertexColoured v = *src; + v16 x = floattov16(v.x / 64.0f); + v16 y = floattov16(v.y / 64.0f); + v16 z = floattov16(v.z / 64.0f); + dst->xy = (y << 16) | (x & 0xFFFF); + dst->z = z; + + int r = PackedCol_R(v.Col); + int g = PackedCol_G(v.Col); + int b = PackedCol_B(v.Col); + dst->rgb = RGB15(r >> 3, g >> 3, b >> 3); + } +} + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return Mem_TryAlloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + buf_fmt = fmt; + buf_count = count; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; + + if (buf_fmt == VERTEX_FORMAT_TEXTURED) { + PreprocessTexturedVertices(); + } else { + PreprocessColouredVertices(); + } +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return Gfx_LockVb(vb, fmt, count); +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { Gfx_UnlockVb(vb); } + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool skipRendering; + +void Gfx_SetFog(cc_bool enabled) { +} + +void Gfx_SetFogCol(PackedCol color) { +} + +void Gfx_SetFogDensity(float value) { +} + +void Gfx_SetFogEnd(float value) { +} + +void Gfx_SetFogMode(FogFunc func) { +} + +static void SetAlphaTest(cc_bool enabled) { + if (enabled) { + //glEnable(GL_ALPHA_TEST); + } else { + //glDisable(GL_ALPHA_TEST); + } +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + skipRendering = depthOnly; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static int matrix_modes[] = { GL_PROJECTION, GL_MODELVIEW, GL_TEXTURE }; +static int lastMatrix; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + + m4x4 m; + const float* src = (const float*)matrix; + + for (int i = 0; i < 4 * 4; i++) + { + m.m[i] = floattof32(src[i]); + } + glLoadMatrix4x4(&m); + + // Vertex commands are signed 16 bit values, with 12 bits fractional + // aka only from -8.0 to 8.0 + // That's way too small to be useful, so counteract that by scaling down + // vertices and then scaling up the matrix multiplication + if (type == MATRIX_VIEW) + glScalef32(floattof32(64.0f), floattof32(64.0f), floattof32(64.0f)); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + if (type != lastMatrix) { lastMatrix = type; glMatrixMode(matrix_modes[type]); } + glLoadIdentity(); +} + +static struct Matrix texMatrix; +void Gfx_EnableTextureOffset(float x, float y) { + texMatrix.row1.x = x; texMatrix.row2.y = y; + Gfx_LoadMatrix(2, &texMatrix); + //glTexParameter(0, TEXGEN_NORMAL | GL_TEXTURE_WRAP_S | GL_TEXTURE_WRAP_T); + +} + +void Gfx_DisableTextureOffset(void) { + texMatrix.row1.x = 0; texMatrix.row1.y = 0; + Gfx_LoadMatrix(2, &texMatrix); + //glTexParameter(0, TEXGEN_TEXCOORD | GL_TEXTURE_WRAP_S | GL_TEXTURE_WRAP_T); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Rendering--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + glEnable(GL_TEXTURE_2D); + } else { + glDisable(GL_TEXTURE_2D); + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { +} + + +static void Draw_ColouredTriangles(int verticesCount, int startVertex) { + glBegin(GL_QUADS); + for (int i = 0; i < verticesCount; i++) + { + struct DSColouredVertex* v = (struct DSColouredVertex*)gfx_vertices + startVertex + i; + + GFX_COLOR = v->rgb; + GFX_VERTEX16 = v->xy; + GFX_VERTEX16 = v->z; + } + glEnd(); +} + +static void Draw_TexturedTriangles(int verticesCount, int startVertex) { + glBegin(GL_QUADS); + int width = 0, height = 0; + glGetInt(GL_GET_TEXTURE_WIDTH, &width); + glGetInt(GL_GET_TEXTURE_HEIGHT, &height); + + // Original code used was + // U = mulf32(v->u, inttof32(width)) + // which behind the scenes expands to + // W = width << 12 + // U = ((int64)v->u * W) >> 12; + // and in this case, the bit shifts can be cancelled out + // to avoid calling __aeabi_lmul to perform the 64 bit multiplication + // therefore the code can be simplified to + // U = v->u * width + + for (int i = 0; i < verticesCount; i++) + { + struct DSTexturedVertex* v = (struct DSTexturedVertex*)gfx_vertices + startVertex + i; + + GFX_COLOR = v->rgb; + GFX_TEX_COORD = TEXTURE_PACK(f32tot16(v->u * width), f32tot16(v->v * height)); + GFX_VERTEX16 = v->xy; + GFX_VERTEX16 = v->z; + } + glEnd(); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + Draw_TexturedTriangles(verticesCount, startVertex); + } else { + Draw_ColouredTriangles(verticesCount, startVertex); + } +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + Draw_TexturedTriangles(verticesCount, 0); + } else { + Draw_ColouredTriangles(verticesCount, 0); + } +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + if (skipRendering) return; + Draw_TexturedTriangles(verticesCount, startVertex); +} +#endif diff --git a/src/Graphics_PS1.c b/src/Graphics_PS1.c new file mode 100644 index 0000000..5417dc6 --- /dev/null +++ b/src/Graphics_PS1.c @@ -0,0 +1,878 @@ +#include "Core.h" +#if defined CC_BUILD_PS1 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Based off https://github.com/Lameguy64/PSn00bSDK/blob/master/examples/beginner/hello/main.c + + +// Length of the ordering table, i.e. the range Z coordinates can have, 0-15 in +// this case. Larger values will allow for more granularity with depth (useful +// when drawing a complex 3D scene) at the expense of RAM usage and performance. +#define OT_LENGTH 1024 + +// Size of the buffer GPU commands and primitives are written to. If the program +// crashes due to too many primitives being drawn, increase this value. +#define BUFFER_LENGTH 32768 + +typedef struct { + DISPENV disp_env; + DRAWENV draw_env; + + cc_uint32 ot[OT_LENGTH]; + cc_uint8 buffer[BUFFER_LENGTH]; +} RenderBuffer; + +static RenderBuffer buffers[2]; +static cc_uint8* next_packet; +static int active_buffer; +static RenderBuffer* buffer; +static void* lastPoly; +static cc_bool cullingEnabled; + +static void OnBufferUpdated(void) { + buffer = &buffers[active_buffer]; + next_packet = buffer->buffer; + ClearOTagR(buffer->ot, OT_LENGTH); +} + +static void SetupContexts(int w, int h, int r, int g, int b) { + SetDefDrawEnv(&buffers[0].draw_env, 0, 0, w, h); + SetDefDispEnv(&buffers[0].disp_env, 0, 0, w, h); + SetDefDrawEnv(&buffers[1].draw_env, 0, h, w, h); + SetDefDispEnv(&buffers[1].disp_env, 0, h, w, h); + + setRGB0(&buffers[0].draw_env, r, g, b); + setRGB0(&buffers[1].draw_env, r, g, b); + buffers[0].draw_env.isbg = 1; + buffers[1].draw_env.isbg = 1; + + active_buffer = 0; + OnBufferUpdated(); +} + +static void FlipBuffers(void) { + DrawSync(0); + VSync(0); + + RenderBuffer* draw_buffer = &buffers[active_buffer]; + RenderBuffer* disp_buffer = &buffers[active_buffer ^ 1]; + + PutDispEnv(&disp_buffer->disp_env); + DrawOTagEnv(&draw_buffer->ot[OT_LENGTH - 1], &draw_buffer->draw_env); + + active_buffer ^= 1; + OnBufferUpdated(); +} + +static void* new_primitive(int size) { + RenderBuffer* buffer = &buffers[active_buffer]; + uint8_t* prim = next_packet; + + next_packet += size; + + assert(next_packet <= &buffer->buffer[BUFFER_LENGTH]); + return (void*)prim; +} + +static GfxResourceID white_square; +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 2x2 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[4] = { BitmapColor_RGB(255, 0, 0), BITMAPCOLOR_WHITE, BITMAPCOLOR_WHITE, BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 2, 2, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +void Gfx_Create(void) { + Gfx.MaxTexWidth = 128; + Gfx.MaxTexHeight = 256; + Gfx.Created = true; + + Gfx_RestoreState(); + ResetGraph(0); + + SetupContexts(Window_Main.Width, Window_Main.Height, 63, 0, 127); + SetDispMask(1); + + InitGeom(); + //gte_SetGeomOffset(Window_Main.Width / 2, Window_Main.Height / 2); + // Set screen depth (basically FOV control, W/2 works best) + //gte_SetGeomScreen(Window_Main.Width / 2); +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +// VRAM can be divided into texture pages +// 32 texture pages total - each page is 64 x 256 +// 10 texture pages are occupied by the doublebuffered display +// 22 texture pages are usable for textures +// These 22 pages are then divided into: +// - 5 for 128+ wide horizontal, 6 otherwise +// - 11 pages for vertical textures +#define TPAGE_WIDTH 64 +#define TPAGE_HEIGHT 256 +#define TPAGES_PER_HALF 16 + +// Horizontally oriented textures (first group of 16) +#define TPAGE_START_HOR 5 +#define MAX_HOR_TEX_PAGES 11 +#define MAX_HOR_TEX_LINES (MAX_HOR_TEX_PAGES * TPAGE_HEIGHT) + +// Horizontally oriented textures (second group of 16) +#define TPAGE_START_VER (16 + 5) +#define MAX_VER_TEX_PAGES 11 +#define MAX_VER_TEX_LINES (MAX_VER_TEX_PAGES * TPAGE_WIDTH) + +static cc_uint8 vram_used[(MAX_HOR_TEX_LINES + MAX_VER_TEX_LINES) / 8]; +#define VRAM_SetUsed(line) (vram_used[(line) / 8] |= (1 << ((line) % 8))) +#define VRAM_UnUsed(line) (vram_used[(line) / 8] &= ~(1 << ((line) % 8))) +#define VRAM_IsUsed(line) (vram_used[(line) / 8] & (1 << ((line) % 8))) + +#define VRAM_BoundingAxis(width, height) height > width ? width : height + +static void VRAM_GetBlockRange(int width, int height, int* beg, int* end) { + if (height > width) { + *beg = MAX_HOR_TEX_LINES; + *end = MAX_HOR_TEX_LINES + MAX_VER_TEX_LINES; + } else if (width >= 128) { + *beg = 0; + *end = 5 * TPAGE_HEIGHT; + } else { + *beg = 5 * TPAGE_HEIGHT; + *end = MAX_HOR_TEX_LINES; + } +} + +static cc_bool VRAM_IsRangeFree(int beg, int end) { + for (int i = beg; i < end; i++) + { + if (VRAM_IsUsed(i)) return false; + } + return true; +} + +static int VRAM_FindFreeBlock(int width, int height) { + int beg, end; + VRAM_GetBlockRange(width, height, &beg, &end); + + // TODO kinda inefficient + for (int i = beg; i < end - height; i++) + { + if (VRAM_IsUsed(i)) continue; + + if (VRAM_IsRangeFree(i, i + height)) return i; + } + return -1; +} + +static void VRAM_AllocBlock(int line, int width, int height) { + int lines = VRAM_BoundingAxis(width, height); + for (int i = line; i < line + lines; i++) + { + VRAM_SetUsed(i); + } +} + +static void VRAM_FreeBlock(int line, int width, int height) { + int lines = VRAM_BoundingAxis(width, height); + for (int i = line; i < line + lines; i++) + { + VRAM_UnUsed(i); + } +} + +static int VRAM_CalcPage(int line) { + if (line < MAX_HOR_TEX_LINES) { + return TPAGE_START_HOR + (line / TPAGE_HEIGHT); + } else { + line -= MAX_HOR_TEX_LINES; + return TPAGE_START_VER + (line / TPAGE_WIDTH); + } +} + + +#define TEXTURES_MAX_COUNT 64 +typedef struct GPUTexture { + cc_uint16 width, height; + cc_uint8 width_mask, height_mask; + cc_uint16 line, tpage; + cc_uint8 xOffset, yOffset; +} GPUTexture; +static GPUTexture textures[TEXTURES_MAX_COUNT]; +static GPUTexture* curTex; + +#define BGRA8_to_PS1(src) \ + ((src[2] & 0xF8) >> 3) | ((src[1] & 0xF8) << 2) | ((src[0] & 0xF8) << 7) | ((src[3] & 0x80) << 8) + +static void* AllocTextureAt(int i, struct Bitmap* bmp, int rowWidth) { + cc_uint16* tmp = Mem_TryAlloc(bmp->width * bmp->height, 2); + if (!tmp) return NULL; + + for (int y = 0; y < bmp->height; y++) + { + cc_uint32* src = bmp->scan0 + y * rowWidth; + cc_uint16* dst = tmp + y * bmp->width; + + for (int x = 0; x < bmp->width; x++) { + cc_uint8* color = (cc_uint8*)&src[x]; + dst[x] = BGRA8_to_PS1(color); + } + } + + GPUTexture* tex = &textures[i]; + int line = VRAM_FindFreeBlock(bmp->width, bmp->height); + if (line == -1) { Mem_Free(tmp); return NULL; } + + tex->width = bmp->width; tex->width_mask = bmp->width - 1; + tex->height = bmp->height; tex->height_mask = bmp->height - 1; + tex->line = line; + + int page = VRAM_CalcPage(line); + int pageX = (page % TPAGES_PER_HALF); + int pageY = (page / TPAGES_PER_HALF); + tex->tpage = (2 << 7) | (pageY << 4) | pageX; + + VRAM_AllocBlock(line, bmp->width, bmp->height); + if (bmp->height > bmp->width) { + tex->xOffset = line % TPAGE_WIDTH; + tex->yOffset = 0; + } else { + tex->xOffset = 0; + tex->yOffset = line % TPAGE_HEIGHT; + } + + Platform_Log3("%i x %i = %i", &bmp->width, &bmp->height, &line); + Platform_Log3(" at %i (%i, %i)", &page, &pageX, &pageY); + + RECT rect; + rect.x = pageX * TPAGE_WIDTH + tex->xOffset; + rect.y = pageY * TPAGE_HEIGHT + tex->yOffset; + rect.w = bmp->width; + rect.h = bmp->height; + + int RX = rect.x, RY = rect.y; + Platform_Log2(" LOAD AT: %i, %i", &RX, &RY); + LoadImage2(&rect, tmp); + + Mem_Free(tmp); + return tex; +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + for (int i = 0; i < TEXTURES_MAX_COUNT; i++) + { + if (textures[i].width) continue; + return AllocTextureAt(i, bmp, rowWidth); + } + + Platform_LogConst("No room for more textures"); + return NULL; +} + +void Gfx_BindTexture(GfxResourceID texId) { + if (!texId) texId = white_square; + curTex = (GPUTexture*)texId; +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GfxResourceID data = *texId; + if (!data) return; + GPUTexture* tex = (GPUTexture*)data; + + VRAM_FreeBlock(tex->line, tex->width, tex->height); + tex->width = 0; tex->height = 0; + *texId = NULL; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + // TODO +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*------------------------------------------------------State management---------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) { } +void Gfx_SetFogCol(PackedCol col) { } +void Gfx_SetFogDensity(float value) { } +void Gfx_SetFogEnd(float value) { } +void Gfx_SetFogMode(FogFunc func) { } + +void Gfx_SetFaceCulling(cc_bool enabled) { + cullingEnabled = enabled; +} + +static void SetAlphaTest(cc_bool enabled) { +} + +static void SetAlphaBlend(cc_bool enabled) { +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_ClearBuffers(GfxBuffers buffers) { +} + +void Gfx_ClearColor(PackedCol color) { + int r = PackedCol_R(color); + int g = PackedCol_G(color); + int b = PackedCol_B(color); + + setRGB0(&buffers[0].draw_env, r, g, b); + setRGB0(&buffers[1].draw_env, r, g, b); +} + +void Gfx_SetDepthTest(cc_bool enabled) { +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + // TODO +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*-------------------------------------------------------Vertex buffers----------------------------------------------------* +*#########################################################################################################################*/ +static void* gfx_vertices; + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return Mem_TryAlloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj, mvp; +#define ToFixed(v) (int)(v * (1 << 12)) + +static void LoadTransformMatrix(struct Matrix* src) { + // https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati + MATRIX mtx; + + mtx.t[0] = 0; + mtx.t[1] = 0; + mtx.t[2] = 0; + + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row1.x, &src->row1.y, &src->row1.z); + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row2.x, &src->row2.y, &src->row2.z); + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row3.x, &src->row3.y, &src->row3.z); + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row4.x, &src->row4.y, &src->row4.z); + //Platform_LogConst("===="); + + float len1 = Math_SqrtF(src->row1.x + src->row1.y + src->row1.z); + float len2 = Math_SqrtF(src->row2.x + src->row2.y + src->row2.z); + float len3 = Math_SqrtF(src->row3.x + src->row3.y + src->row3.z); + + mtx.m[0][0] = ToFixed(1); + mtx.m[0][1] = 0; + mtx.m[0][2] = 0; + + mtx.m[1][0] = 0; + mtx.m[1][1] = ToFixed(1); + mtx.m[1][2] = 0; + + mtx.m[2][0] = 0; + mtx.m[2][1] = ToFixed(1); + mtx.m[2][2] = 1; + + gte_SetRotMatrix(&mtx); + gte_SetTransMatrix(&mtx); +} + +/*static void LoadTransformMatrix(struct Matrix* src) { + // https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati + MATRIX mtx; + + mtx.t[0] = ToFixed(src->row4.x); + mtx.t[1] = ToFixed(src->row4.y); + mtx.t[2] = ToFixed(src->row4.z); + + Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row1.x, &src->row1.y, &src->row1.z); + Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row2.x, &src->row2.y, &src->row2.z); + Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row3.x, &src->row3.y, &src->row3.z); + Platform_Log3("X: %f3, Y: %f3, Z: %f3", &src->row4.x, &src->row4.y, &src->row4.z); + Platform_LogConst("===="); + + float len1 = Math_SqrtF(src->row1.x + src->row1.y + src->row1.z); + float len2 = Math_SqrtF(src->row2.x + src->row2.y + src->row2.z); + float len3 = Math_SqrtF(src->row3.x + src->row3.y + src->row3.z); + + mtx.m[0][0] = ToFixed(src->row1.x / len1); + mtx.m[0][1] = ToFixed(src->row1.y / len1); + mtx.m[0][2] = ToFixed(src->row1.z / len1); + + mtx.m[1][0] = ToFixed(src->row2.x / len2); + mtx.m[1][1] = ToFixed(src->row2.y / len2); + mtx.m[1][2] = ToFixed(src->row2.z / len2); + + mtx.m[2][0] = ToFixed(src->row3.x / len3); + mtx.m[2][1] = ToFixed(src->row3.y / len3); + mtx.m[2][2] = ToFixed(src->row3.z / len3); + + gte_SetRotMatrix(&mtx); + gte_SetTransMatrix(&mtx); +}*/ + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + Matrix_Mul(&mvp, &_view, &_proj); + LoadTransformMatrix(&mvp); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + // TODO +} + +void Gfx_DisableTextureOffset(void) { + // TODO +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.01f; + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh */ + float c = (float)Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zNear - zFar); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Rendering-------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; +} + +void Gfx_DrawVb_Lines(int verticesCount) { + +} + +static void Transform(Vec3* result, struct VertexTextured* a, const struct Matrix* mat) { + /* a could be pointing to result - therefore can't directly assign X/Y/Z */ + float x = a->x * mat->row1.x + a->y * mat->row2.x + a->z * mat->row3.x + mat->row4.x; + float y = a->x * mat->row1.y + a->y * mat->row2.y + a->z * mat->row3.y + mat->row4.y; + float z = a->x * mat->row1.z + a->y * mat->row2.z + a->z * mat->row3.z + mat->row4.z; + float w = a->x * mat->row1.w + a->y * mat->row2.w + a->z * mat->row3.w + mat->row4.w; + + result->x = (x/w) * (320/2) + (320/2); + result->y = (y/w) * -(240/2) + (240/2); + result->z = (z/w) * OT_LENGTH; +} + +cc_bool VERTEX_LOGGING; + +static void DrawColouredQuads2D(int verticesCount, int startVertex) { + return; + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexColoured* v = (struct VertexColoured*)gfx_vertices + startVertex + i; + + POLY_F4* poly = new_primitive(sizeof(POLY_F4)); + setPolyF4(poly); + + poly->x0 = v[1].x; poly->y0 = v[1].y; + poly->x1 = v[0].x; poly->y1 = v[0].y; + poly->x2 = v[2].x; poly->y2 = v[2].y; + poly->x3 = v[3].x; poly->y3 = v[3].y; + + poly->r0 = PackedCol_R(v->Col); + poly->g0 = PackedCol_G(v->Col); + poly->b0 = PackedCol_B(v->Col); + + if (lastPoly) { + setaddr(poly, getaddr(lastPoly)); setaddr(lastPoly, poly); + } else { + addPrim(&buffer->ot[0], poly); + } + lastPoly = poly; + } +} + +static void DrawTexturedQuads2D(int verticesCount, int startVertex) { + int uOffset = curTex->xOffset, vOffset = curTex->yOffset; + + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + POLY_FT4* poly = new_primitive(sizeof(POLY_FT4)); + setPolyFT4(poly); + poly->tpage = curTex->tpage; + poly->clut = 0; + + // TODO & instead of % + poly->x0 = v[1].x; poly->y0 = v[1].y; + poly->x1 = v[0].x; poly->y1 = v[0].y; + poly->x2 = v[2].x; poly->y2 = v[2].y; + poly->x3 = v[3].x; poly->y3 = v[3].y; + + poly->u0 = ((int)(v[1].U * 0.99f * curTex->width) & curTex->width_mask) + uOffset; + poly->v0 = ((int)(v[1].V * curTex->height) & curTex->height_mask) + vOffset; + poly->u1 = ((int)(v[0].U * 0.99f * curTex->width) & curTex->width_mask) + uOffset; + poly->v1 = ((int)(v[0].V * curTex->height) & curTex->height_mask) + vOffset; + poly->u2 = ((int)(v[2].U * 0.99f * curTex->width) & curTex->width_mask) + uOffset; + poly->v2 = ((int)(v[2].V * curTex->height) & curTex->height_mask) + vOffset; + poly->u3 = ((int)(v[3].U * 0.99f * curTex->width) & curTex->width_mask) + uOffset; + poly->v3 = ((int)(v[3].V * curTex->height) & curTex->height_mask) + vOffset; + + // https://problemkaputt.de/psxspx-gpu-rendering-attributes.htm + // "For untextured graphics, 8bit RGB values of FFh are brightest. However, for texture blending, 8bit values of 80h are brightest" + poly->r0 = PackedCol_R(v->Col) >> 1; + poly->g0 = PackedCol_G(v->Col) >> 1; + poly->b0 = PackedCol_B(v->Col) >> 1; + + if (lastPoly) { + setaddr(poly, getaddr(lastPoly)); setaddr(lastPoly, poly); + } else { + addPrim(&buffer->ot[0], poly); + } + lastPoly = poly; + } +} + +static void DrawColouredQuads3D(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexColoured* v = (struct VertexColoured*)gfx_vertices + startVertex + i; + + POLY_F4* poly = new_primitive(sizeof(POLY_F4)); + setPolyF4(poly); + + Vec3 coords[4]; + Transform(&coords[0], &v[0], &mvp); + Transform(&coords[1], &v[1], &mvp); + Transform(&coords[2], &v[2], &mvp); + Transform(&coords[3], &v[3], &mvp); + + poly->x0 = coords[1].x; poly->y0 = coords[1].y; + poly->x1 = coords[0].x; poly->y1 = coords[0].y; + poly->x2 = coords[2].x; poly->y2 = coords[2].y; + poly->x3 = coords[3].x; poly->y3 = coords[3].y; + + int p = (coords[0].z + coords[1].z + coords[2].z + coords[3].z) / 4; + if (p < 0 || p >= OT_LENGTH) continue; + + int X = v[0].x, Y = v[0].y, Z = v[0].z; + //if (VERTEX_LOGGING) Platform_Log3("IN: %i, %i, %i", &X, &Y, &Z); + X = poly->x1; Y = poly->y1, Z = coords[0].z; + + poly->r0 = PackedCol_R(v->Col); + poly->g0 = PackedCol_G(v->Col); + poly->b0 = PackedCol_B(v->Col); + //if (VERTEX_LOGGING) Platform_Log4("OUT: %i, %i, %i (%i)", &X, &Y, &Z, &p); + + addPrim(&buffer->ot[p >> 2], poly); + } +} + +static void DrawTexturedQuads3D(int verticesCount, int startVertex) { + int uOffset = curTex->xOffset, vOffset = curTex->yOffset; + + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + POLY_FT4* poly = new_primitive(sizeof(POLY_FT4)); + setPolyFT4(poly); + poly->tpage = curTex->tpage; + poly->clut = 0; + + Vec3 coords[4]; + Transform(&coords[0], &v[0], &mvp); + Transform(&coords[1], &v[1], &mvp); + Transform(&coords[2], &v[2], &mvp); + Transform(&coords[3], &v[3], &mvp); + + // TODO & instead of % + poly->x0 = coords[1].x; poly->y0 = coords[1].y; + poly->x1 = coords[0].x; poly->y1 = coords[0].y; + poly->x2 = coords[2].x; poly->y2 = coords[2].y; + poly->x3 = coords[3].x; poly->y3 = coords[3].y; + + if (cullingEnabled) { + // https://gamedev.stackexchange.com/questions/203694/how-to-make-backface-culling-work-correctly-in-both-orthographic-and-perspective + int signA = (poly->x0 - poly->x1) * (poly->y2 - poly->y1); + int signB = (poly->x2 - poly->x1) * (poly->y0 - poly->y1); + if (signA > signB) continue; + } + + poly->u0 = ((int)(v[1].U * curTex->width) & curTex->width_mask) + uOffset; + poly->v0 = ((int)(v[1].V * curTex->height) & curTex->height_mask) + vOffset; + poly->u1 = ((int)(v[0].U * curTex->width) & curTex->width_mask) + uOffset; + poly->v1 = ((int)(v[0].V * curTex->height) & curTex->height_mask) + vOffset; + poly->u2 = ((int)(v[2].U * curTex->width) & curTex->width_mask) + uOffset; + poly->v2 = ((int)(v[2].V * curTex->height) & curTex->height_mask) + vOffset; + poly->u3 = ((int)(v[3].U * curTex->width) & curTex->width_mask) + uOffset; + poly->v3 = ((int)(v[3].V * curTex->height) & curTex->height_mask) + vOffset; + + //int P = curTex->height, page = poly->tpage & 0xFF, ll = curTex->yOffset; + //Platform_Log4("XYZ: %f3 x %i, %i, %i", &v[0].V, &P, &page, &ll); + int p = (coords[0].z + coords[1].z + coords[2].z + coords[3].z) / 4; + if (p < 0 || p >= OT_LENGTH) continue; + + int X = v[0].x, Y = v[0].y, Z = v[0].z; + //if (VERTEX_LOGGING) Platform_Log3("IN: %i, %i, %i", &X, &Y, &Z); + X = poly->x1; Y = poly->y1, Z = coords[0].z; + + poly->r0 = PackedCol_R(v->Col) >> 1; + poly->g0 = PackedCol_G(v->Col) >> 1; + poly->b0 = PackedCol_B(v->Col) >> 1; + //if (VERTEX_LOGGING) Platform_Log4("OUT: %i, %i, %i (%i)", &X, &Y, &Z, &p); + + // TODO: 2D shouldn't use AddPrim, draws in the wrong way + addPrim(&buffer->ot[p >> 2], poly); + } +} + +/*static void DrawQuads(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + POLY_F4* poly = new_primitive(sizeof(POLY_F4)); + setPolyF4(poly); + + SVECTOR coords[4]; + coords[0].vx = v[0].x; coords[0].vy = v[0].y; coords[0].vz = v[0].z; + coords[1].vx = v[1].x; coords[1].vy = v[1].y; coords[1].vz = v[1].z; + coords[2].vx = v[2].x; coords[2].vy = v[2].y; coords[2].vz = v[1].z; + coords[3].vx = v[3].x; coords[3].vy = v[3].y; coords[3].vz = v[3].z; + + int X = coords[0].vx, Y = coords[0].vy, Z = coords[0].vz; + //Platform_Log3("IN: %i, %i, %i", &X, &Y, &Z); + gte_ldv3(&coords[0], &coords[1], &coords[2]); + gte_rtpt(); + gte_stsxy0(&poly->x0); + + int p; + gte_avsz3(); + gte_stotz( &p ); + + X = poly->x0; Y = poly->y0, Z = p; + //Platform_Log3("OUT: %i, %i, %i", &X, &Y, &Z); + if (((p >> 2) >= OT_LENGTH) || ((p >> 2) < 0)) + continue; + + gte_ldv0(&coords[3]); + gte_rtps(); + gte_stsxy3(&poly->x1, &poly->x2, &poly->x3); + + //poly->x0 = v[1].x; poly->y0 = v[1].y; + //poly->x1 = v[0].x; poly->y1 = v[0].y; + //poly->x2 = v[2].x; poly->y2 = v[2].y; + //poly->x3 = v[3].x; poly->y3 = v[3].y; + + poly->r0 = PackedCol_R(v->Col); + poly->g0 = PackedCol_G(v->Col); + poly->b0 = PackedCol_B(v->Col); + + addPrim(&buffer->ot[p >> 2], poly); + } +}*/ + +/*static void DrawQuads(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + POLY_F4* poly = new_primitive(sizeof(POLY_F4)); + setPolyF4(poly); + + poly->x0 = v[1].x; poly->y0 = v[1].y; + poly->x1 = v[0].x; poly->y1 = v[0].y; + poly->x2 = v[2].x; poly->y2 = v[2].y; + poly->x3 = v[3].x; poly->y3 = v[3].y; + + poly->r0 = PackedCol_R(v->Col); + poly->g0 = PackedCol_G(v->Col); + poly->b0 = PackedCol_B(v->Col); + + int p = 0; + addPrim(&buffer->ot[p >> 2], poly); + } +}*/ + +static void DrawQuads(int verticesCount, int startVertex) { + if (gfx_rendering2D && gfx_format == VERTEX_FORMAT_TEXTURED) { + DrawTexturedQuads2D(verticesCount, startVertex); + } else if (gfx_rendering2D) { + DrawColouredQuads2D(verticesCount, startVertex); + } else if (gfx_format == VERTEX_FORMAT_TEXTURED) { + DrawTexturedQuads3D(verticesCount, startVertex); + } else { + DrawColouredQuads3D(verticesCount, startVertex); + } +} + + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + DrawQuads(verticesCount, startVertex); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + DrawQuads(verticesCount, 0); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + DrawTexturedQuads3D(verticesCount, startVertex); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Other/Misc------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +cc_bool Gfx_WarnIfNecessary(void) { + return false; +} + +void Gfx_BeginFrame(void) { + lastPoly = NULL; +} + +void Gfx_EndFrame(void) { + FlipBuffers(); + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_OnWindowResize(void) { + // TODO +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using PS1 --\n"); + PrintMaxTextureInfo(info); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } + +void Gfx_Begin2D(int width, int height) { + gfx_rendering2D = true; + Gfx_SetAlphaBlending(true); +} + +void Gfx_End2D(void) { + gfx_rendering2D = false; + Gfx_SetAlphaBlending(false); +} +#endif diff --git a/src/Graphics_PS2.c b/src/Graphics_PS2.c new file mode 100644 index 0000000..9eeb9d2 --- /dev/null +++ b/src/Graphics_PS2.c @@ -0,0 +1,666 @@ +#include "Core.h" +#if defined CC_BUILD_PS2 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void* gfx_vertices; +extern framebuffer_t fb_colors[2]; +extern zbuffer_t fb_depth; +static float vp_hwidth, vp_hheight; + +// double buffering +static packet_t* packets[2]; +static packet_t* current; +static int context; +static qword_t* dma_tag; +static qword_t* q; + +static GfxResourceID white_square; +static int primitive_type; + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 16x16 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[16 * 16]; + Mem_Set(pixels, 0xFF, sizeof(pixels)); + Bitmap_Init(bmp, 16, 16, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +static qword_t* SetTextureWrapping(qword_t* q, int context) { + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + + PACK_GIFTAG(q, GS_SET_CLAMP(WRAP_REPEAT, WRAP_REPEAT, 0, 0, 0, 0), + GS_REG_CLAMP + context); + q++; + return q; +} + +static qword_t* SetTextureSampling(qword_t* q, int context) { + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + + // TODO: should mipmapselect (first 0 after MIN_NEAREST) be 1? + PACK_GIFTAG(q, GS_SET_TEX1(LOD_USE_K, 0, LOD_MAG_NEAREST, LOD_MIN_NEAREST, 0, 0, 0), + GS_REG_TEX1 + context); + q++; + return q; +} + +static qword_t* SetAlphaBlending(qword_t* q, int context) { + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + + // https://psi-rockin.github.io/ps2tek/#gsalphablending + // Output = (((A - B) * C) >> 7) + D + // = (((src - dst) * alpha) >> 7) + dst + // = (src * alpha - dst * alpha) / 128 + dst + // = (src * alpha - dst * alpha) / 128 + dst * 128 / 128 + // = ((src * alpha + dst * (128 - alpha)) / 128 + PACK_GIFTAG(q, GS_SET_ALPHA(BLEND_COLOR_SOURCE, BLEND_COLOR_DEST, BLEND_ALPHA_SOURCE, + BLEND_COLOR_DEST, 0x80), GS_REG_ALPHA + context); + q++; + return q; +} + +static void InitDrawingEnv(void) { + packet_t *packet = packet_init(30, PACKET_NORMAL); // TODO: is 30 too much? + qword_t *q = packet->data; + + q = draw_setup_environment(q, 0, &fb_colors[0], &fb_depth); + // GS can render from 0 to 4096, so set primitive origin to centre of that + q = draw_primitive_xyoffset(q, 0, 2048 - vp_hwidth, 2048 - vp_hheight); + + q = SetTextureWrapping(q, 0); + q = SetTextureSampling(q, 0); + q = SetAlphaBlending(q, 0); // TODO has no effect ? + q = draw_finish(q); + + dma_channel_send_normal(DMA_CHANNEL_GIF,packet->data,q - packet->data, 0, 0); + dma_wait_fast(); + + packet_free(packet); +} + +// TODO: Find a better way than just increasing this hardcoded size +static void InitDMABuffers(void) { + packets[0] = packet_init(50000, PACKET_NORMAL); + packets[1] = packet_init(50000, PACKET_NORMAL); +} + +static void FlipContext(void) { + context ^= 1; + current = packets[context]; + + dma_tag = current->data; + // increment past the dmatag itself + q = dma_tag + 1; +} + +static int tex_offset; +void Gfx_Create(void) { + vp_hwidth = DisplayInfo.Width / 2; + vp_hheight = DisplayInfo.Height / 2; + primitive_type = 0; // PRIM_POINT, which isn't used here + + InitDrawingEnv(); + InitDMABuffers(); + tex_offset = graph_vram_allocate(256, 256, GS_PSM_32, GRAPH_ALIGN_BLOCK); + + context = 1; + FlipContext(); + +// TODO maybe Min not actually needed? + Gfx.MinTexWidth = 4; + Gfx.MinTexHeight = 4; + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.MaxTexSize = 512 * 512; + Gfx.Created = true; + + Gfx_RestoreState(); +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCTexture_ { + cc_uint32 width, height; + cc_uint32 log2_width, log2_height; + cc_uint32 pad[(64 - 16)/4]; + cc_uint32 pixels[]; // aligned to 64 bytes (only need 16?) +} CCTexture; + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + int size = bmp->width * bmp->height * 4; + CCTexture* tex = (CCTexture*)memalign(16, 64 + size); + + tex->width = bmp->width; + tex->height = bmp->height; + tex->log2_width = draw_log2(bmp->width); + tex->log2_height = draw_log2(bmp->height); + + CopyTextureData(tex->pixels, bmp->width * 4, bmp, rowWidth << 2); + return tex; +} + +static void UpdateTextureBuffer(int context, texbuffer_t *texture, CCTexture* tex) { + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + + PACK_GIFTAG(q, GS_SET_TEX0(texture->address >> 6, texture->width >> 6, GS_PSM_32, + tex->log2_width, tex->log2_height, TEXTURE_COMPONENTS_RGBA, TEXTURE_FUNCTION_MODULATE, + 0, 0, CLUT_STORAGE_MODE1, 0, CLUT_NO_LOAD), GS_REG_TEX0 + context); + q++; +} + +void Gfx_BindTexture(GfxResourceID texId) { + if (!texId) texId = white_square; + CCTexture* tex = (CCTexture*)texId; + + texbuffer_t texbuf; + texbuf.width = max(256, tex->width); + texbuf.address = tex_offset; + + // TODO terrible perf + DMATAG_END(dma_tag, (q - current->data) - 1, 0, 0, 0); + dma_channel_send_chain(DMA_CHANNEL_GIF, current->data, q - current->data, 0, 0); + dma_wait_fast(); + + packet_t *packet = packet_init(200, PACKET_NORMAL); + + qword_t *Q = packet->data; + + Q = draw_texture_transfer(Q, tex->pixels, tex->width, tex->height, GS_PSM_32, tex_offset, max(256, tex->width)); + Q = draw_texture_flush(Q); + + dma_channel_send_chain(DMA_CHANNEL_GIF,packet->data, Q - packet->data, 0,0); + dma_wait_fast(); + + packet_free(packet); + + // TODO terrible perf + q = dma_tag + 1; + UpdateTextureBuffer(0, &texbuf, tex); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GfxResourceID data = *texId; + if (data) Mem_Free(data); + *texId = NULL; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + // TODO +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*------------------------------------------------------State management---------------------------------------------------* +*#########################################################################################################################*/ +static int clearR, clearG, clearB; +static cc_bool gfx_depthTest; +static cc_bool stateDirty; + +void Gfx_SetFog(cc_bool enabled) { } +void Gfx_SetFogCol(PackedCol col) { } +void Gfx_SetFogDensity(float value) { } +void Gfx_SetFogEnd(float value) { } +void Gfx_SetFogMode(FogFunc func) { } + +// +static void UpdateState(int context) { + // TODO: toggle Enable instead of method ? + int aMethod = gfx_alphaTest ? ATEST_METHOD_GREATER_EQUAL : ATEST_METHOD_ALLPASS; + int zMethod = gfx_depthTest ? ZTEST_METHOD_GREATER_EQUAL : ZTEST_METHOD_ALLPASS; + + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + PACK_GIFTAG(q, GS_SET_TEST(DRAW_ENABLE, aMethod, 0x80, ATEST_KEEP_FRAMEBUFFER, + DRAW_DISABLE, DRAW_DISABLE, + DRAW_ENABLE, zMethod), GS_REG_TEST + context); + q++; + + stateDirty = false; +} + +void Gfx_SetFaceCulling(cc_bool enabled) { + // TODO +} + +static void SetAlphaTest(cc_bool enabled) { + stateDirty = true; +} + +static void SetAlphaBlend(cc_bool enabled) { + // TODO update primitive state +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_ClearBuffers(GfxBuffers buffers) { + // TODO clear only some buffers + q = draw_disable_tests(q, 0, &fb_depth); + q = draw_clear(q, 0, 2048.0f - fb_colors[0].width / 2.0f, 2048.0f - fb_colors[0].height / 2.0f, + fb_colors[0].width, fb_colors[0].height, clearR, clearG, clearB); + UpdateState(0); +} + +void Gfx_ClearColor(PackedCol color) { + clearR = PackedCol_R(color); + clearG = PackedCol_G(color); + clearB = PackedCol_B(color); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + gfx_depthTest = enabled; + stateDirty = true; +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + // TODO +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*-------------------------------------------------------Vertex buffers----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return Mem_TryAlloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj, mvp; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + Matrix_Mul(&mvp, &_view, &_proj); + // TODO +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); + // TODO +} + +void Gfx_EnableTextureOffset(float x, float y) { + // TODO +} + +void Gfx_DisableTextureOffset(void) { + // TODO +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -2.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -(zFar + zNear) / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear_ = zFar; + float zFar_ = 0.1f; + float c = Cotangent(0.5f * fov); + + /* Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum */ + /* For pos FOV based perspective matrix, left/right/top/bottom are calculated as: */ + /* left = -c * aspect, right = c * aspect, bottom = -c, top = c */ + /* Calculations are simplified because of left/right and top/bottom symmetry */ + *matrix = Matrix_Identity; + // TODO: Check is Frustum culling needs changing for this + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar_ + zNear_) / (zFar_ - zNear_); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar_ * zNear_) / (zFar_ - zNear_); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Rendering-------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + // TODO update cached primitive state +} + +typedef struct Vector4 { float x, y, z, w; } Vector4; + +static cc_bool NotClipped(Vector4 pos) { + // The code below clips to the viewport clip planes + // For e.g. X this is [2048 - vp_width / 2, 2048 + vp_width / 2] + // However the guard band itself ranges from 0 to 4096 + // To reduce need to clip, clip against guard band on X/Y axes instead + /*return + xAdj >= -pos.w && xAdj <= pos.w && + yAdj >= -pos.w && yAdj <= pos.w && + pos.z >= -pos.w && pos.z <= pos.w;*/ + + // Rescale clip planes to guard band extent: + // X/W * vp_hwidth <= vp_hwidth -- clipping against viewport + // X/W <= 1 + // X <= W + // X/W * vp_hwidth <= 2048 -- clipping against guard band + // X/W <= 2048 / vp_hwidth + // X * vp_hwidth / 2048 <= W + float xAdj = pos.x * (vp_hwidth/2048); + float yAdj = pos.y * (vp_hheight/2048); + + // X/W * vp_hwidth <= 2048 + // + + // Clip X/Y to INSIDE the guard band regions + // NOTE: This seems to result in lockup at end of frame + return + xAdj > -pos.w && xAdj < pos.w && + yAdj > -pos.w && yAdj < pos.w && + pos.z >= -pos.w && pos.z <= pos.w; +} + +static Vector4 TransformVertex(struct VertexTextured* pos) { + Vector4 coord; + coord.x = pos->x * mvp.row1.x + pos->y * mvp.row2.x + pos->z * mvp.row3.x + mvp.row4.x; + coord.y = pos->x * mvp.row1.y + pos->y * mvp.row2.y + pos->z * mvp.row3.y + mvp.row4.y; + coord.z = pos->x * mvp.row1.z + pos->y * mvp.row2.z + pos->z * mvp.row3.z + mvp.row4.z; + coord.w = pos->x * mvp.row1.w + pos->y * mvp.row2.w + pos->z * mvp.row3.w + mvp.row4.w; + return coord; +} + +//#define VCopy(dst, src) dst.x = vp_hwidth * (1 + src.x / src.w); dst.y = vp_hheight * (1 - src.y / src.w); dst.z = src.z / src.w; dst.w = src.w; +static xyz_t FinishVertex(struct Vector4 src, float invW) { + float x = (vp_hwidth /2048) * (src.x * invW); + float y = (vp_hheight/2048) * (src.y * invW); + float z = src.z * invW; + + int originX = ftoi4(2048); + int originY = ftoi4(2048); + unsigned int maxZ = 1 << (32 - 1); // TODO: half this? or << 24 instead? + + xyz_t xyz; + xyz.x = (short)((x + 1.0f) * originX); + xyz.y = (short)((y + 1.0f) * -originY); + xyz.z = (unsigned int)((z + 1.0f) * maxZ); + return xyz; +} + +static void DrawTriangle(Vector4 v0, Vector4 v1, Vector4 v2, struct VertexTextured* V0, struct VertexTextured* V1, struct VertexTextured* V2) { + //Platform_Log4("X: %f3, Y: %f3, Z: %f3, W: %f3", &v0.x, &v0.y, &v0.z, &v0.w); + u64* dw; + + Vector4 verts[3] = { v0, v1, v2 }; + struct VertexTextured* v[] = { V0, V1, V2 }; + + //Platform_Log4(" X: %f3, Y: %f3, Z: %f3, W: %f3", &in_vertices[0].x, &in_vertices[0].y, &in_vertices[0].z, &in_vertices[0].w); + + // 3 "primitives" follow in the GIF packet (vertices in this case) + // 2 registers per "primitive" (colour, position) + PACK_GIFTAG(q, GIF_SET_TAG(3,1,0,0, GIF_FLG_REGLIST, 3), DRAW_STQ_REGLIST); + q++; + + // TODO optimise + // Add the "primitives" to the GIF packet + dw = (u64*)q; + for (int i = 0; i < 3; i++) + { + float Q = 1.0f / verts[i].w; + xyz_t xyz = FinishVertex(verts[i], Q); + color_t color; + texel_t texel; + + // See 'Colour Functions' https://psi-rockin.github.io/ps2tek/#gstextures + // Essentially, colour blending is calculated as + // finalR = (vertexR * textureR) >> 7 + // However, this behaves contrary to standard expectations + // and results in final vertex colour being too bright + // + // For instance, if vertexR was white and textureR was grey: + // finalR = (255 * 127) / 128 = 255 + // White would be produced as the final colour instead of expected grey + // + // To counteract this, just divide all vertex colours by 2 first + color.rgbaq = (v[i]->Col & 0xFEFEFEFE) >> 1; + color.q = Q; + texel.u = v[i]->U * Q; + texel.v = v[i]->V * Q; + + *dw++ = color.rgbaq; + *dw++ = texel.uv; + *dw++ = xyz.xyz; + } + dw++; // one more to even out number of doublewords + q = (qword_t*)dw; +} + +static void DrawTriangles(int verticesCount, int startVertex) { + if (stateDirty) UpdateState(0); + if (gfx_format == VERTEX_FORMAT_COLOURED) return; + cc_bool texturing = gfx_format == VERTEX_FORMAT_TEXTURED; + + // TODO: only when vertex format changes ? + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + PACK_GIFTAG(q, GS_SET_PRIM(PRIM_TRIANGLE, PRIM_SHADE_GOURAUD, texturing, DRAW_DISABLE, + gfx_alphaBlend, DRAW_DISABLE, PRIM_MAP_ST, + 0, PRIM_UNFIXED), GS_REG_PRIM); + q++; + + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex; + for (int i = 0; i < verticesCount / 4; i++, v += 4) + { + Vector4 V0 = TransformVertex(v + 0); + Vector4 V1 = TransformVertex(v + 1); + Vector4 V2 = TransformVertex(v + 2); + Vector4 V3 = TransformVertex(v + 3); + + + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &v[0].x, &v[0].y, &v[0].z); + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &v[1].x, &v[1].y, &v[1].z); + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &v[2].x, &v[2].y, &v[2].z); + //Platform_Log3("X: %f3, Y: %f3, Z: %f3", &v[3].x, &v[3].y, &v[3].z); + //Platform_LogConst(">>>>>>>>>>"); + + if (NotClipped(V0) && NotClipped(V1) && NotClipped(V2)) { + DrawTriangle(V0, V1, V2, v + 0, v + 1, v + 2); + } + + if (NotClipped(V2) && NotClipped(V3) && NotClipped(V0)) { + DrawTriangle(V2, V3, V0, v + 2, v + 3, v + 0); + } + + //Platform_LogConst("-----"); + } +} + +// TODO: Can this be used? need to understand EOP more +static void SetPrimitiveType(int type) { + if (primitive_type == type) return; + primitive_type = type; + + PACK_GIFTAG(q, GIF_SET_TAG(1,0,0,0, GIF_FLG_PACKED, 1), GIF_REG_AD); + q++; + PACK_GIFTAG(q, GS_SET_PRIM(type, PRIM_SHADE_GOURAUD, DRAW_DISABLE, DRAW_DISABLE, + DRAW_DISABLE, DRAW_DISABLE, PRIM_MAP_ST, + 0, PRIM_UNFIXED), GS_REG_PRIM); + q++; +} + +void Gfx_DrawVb_Lines(int verticesCount) { + //SetPrimitiveType(PRIM_LINE); +} /* TODO */ + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + //SetPrimitiveType(PRIM_TRIANGLE); + DrawTriangles(verticesCount, startVertex); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + //SetPrimitiveType(PRIM_TRIANGLE); + DrawTriangles(verticesCount, 0); + // TODO +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + //SetPrimitiveType(PRIM_TRIANGLE); + DrawTriangles(verticesCount, startVertex); + // TODO +} + + +/*########################################################################################################################* +*---------------------------------------------------------Other/Misc------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +cc_bool Gfx_WarnIfNecessary(void) { + return false; +} + +void Gfx_BeginFrame(void) { + Platform_LogConst("--- Frame ---"); +} + +void Gfx_EndFrame(void) { + Platform_LogConst("--- EF1 ---"); + // Double buffering + graph_set_framebuffer_filtered(fb_colors[context].address, + fb_colors[context].width, + fb_colors[context].psm, 0, 0); + + q = draw_framebuffer(q, 0, &fb_colors[context ^ 1]); + q = draw_finish(q); + + // Fill out and then send DMA chain + DMATAG_END(dma_tag, (q - current->data) - 1, 0, 0, 0); + dma_wait_fast(); + dma_channel_send_chain(DMA_CHANNEL_GIF, current->data, q - current->data, 0, 0); + Platform_LogConst("--- EF2 ---"); + + draw_wait_finish(); + Platform_LogConst("--- EF3 ---"); + + if (gfx_vsync) graph_wait_vsync(); + if (gfx_minFrameMs) LimitFPS(); + + FlipContext(); + Platform_LogConst("--- EF4 ---"); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_OnWindowResize(void) { + // TODO +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using PS2 --\n"); + PrintMaxTextureInfo(info); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } +#endif diff --git a/src/Graphics_PS3.c b/src/Graphics_PS3.c new file mode 100644 index 0000000..1fbe3f8 --- /dev/null +++ b/src/Graphics_PS3.c @@ -0,0 +1,703 @@ +#include "Core.h" +#if defined CC_BUILD_PS3 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include +#include +#include +static cc_bool renderingDisabled; + +static gcmContextData* context; +static u32 cur_fb; + +#define CB_SIZE 0x100000 // TODO: smaller command buffer? +#define HOST_SIZE (32 * 1024 * 1024) + + +/*########################################################################################################################* +*----------------------------------------------------- Vertex Shaders ----------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCVertexProgram { + rsxVertexProgram* prog; + void* ucode; + rsxProgramConst* mvp; + rsxProgramConst* uv_offset; +} VertexProgram; + +extern const u8 vs_coloured_vpo[]; +extern const u8 vs_textured_vpo[]; +extern const u8 vs_offset_vpo[]; + +static VertexProgram VP_list[3]; +static VertexProgram* VP_active; + +static cc_bool textureOffseting; +static float textureOffset[4] __attribute__((aligned(16))); +static struct Matrix mvp __attribute__((aligned(64))); + + +static void VP_Load(VertexProgram* vp, const u8* source) { + vp->prog = (rsxVertexProgram*)source; + u32 size = 0; + rsxVertexProgramGetUCode(vp->prog, &vp->ucode, &size); + + vp->mvp = rsxVertexProgramGetConst(vp->prog, "mvp"); + vp->uv_offset = rsxVertexProgramGetConst(vp->prog, "uv_offset"); +} + +static void LoadVertexPrograms(void) { + VP_Load(&VP_list[0], vs_coloured_vpo); + VP_Load(&VP_list[1], vs_textured_vpo); + VP_Load(&VP_list[2], vs_offset_vpo); +} + +static void VP_SwitchActive(void) { + int index = gfx_format == VERTEX_FORMAT_TEXTURED ? 1 : 0; + if (textureOffseting) index = 2; + + VertexProgram* VP = &VP_list[index]; + if (VP == VP_active) return; + VP_active = VP; + + rsxLoadVertexProgram(context, VP->prog, VP->ucode); +} + +static void VP_UpdateUniforms() { + // TODO: dirty uniforms instead + for (int i = 0; i < Array_Elems(VP_list); i++) + { + VertexProgram* vp = &VP_list[i]; + rsxSetVertexProgramParameter(context, vp->prog, vp->mvp, (float*)&mvp); + } + + if (VP_active == &VP_list[2]) { + VertexProgram* vp = &VP_list[2]; + rsxSetVertexProgramParameter(context, vp->prog, vp->uv_offset, textureOffset); + } +} + + +/*########################################################################################################################* +*---------------------------------------------------- Fragment Shaders ---------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCFragmentProgram { + rsxFragmentProgram* prog; + void* ucode; + u32* buffer; + u32 offset; +} FragmentProgram; + +extern const u8 ps_textured_fpo[]; +extern const u8 ps_coloured_fpo[]; + +static FragmentProgram FP_list[2]; +static FragmentProgram* FP_active; + + +static void FP_Load(FragmentProgram* fp, const u8* source) { + fp->prog = (rsxFragmentProgram*)source; + u32 size = 0; + rsxFragmentProgramGetUCode(fp->prog, &fp->ucode, &size); + + fp->buffer = (u32*)rsxMemalign(128, size); + Mem_Copy(fp->buffer, fp->ucode, size); + rsxAddressToOffset(fp->buffer, &fp->offset); +} + +static void LoadFragmentPrograms(void) { + FP_Load(&FP_list[0], ps_coloured_fpo); + FP_Load(&FP_list[1], ps_textured_fpo); +} + +static void FP_SwitchActive(void) { + int index = gfx_format == VERTEX_FORMAT_TEXTURED ? 1 : 0; + + FragmentProgram* FP = &FP_list[index]; + if (FP == FP_active) return; + FP_active = FP; + + rsxLoadFragmentProgramLocation(context, FP->prog, FP->offset, GCM_LOCATION_RSX); +} + + +/*########################################################################################################################* +*---------------------------------------------------------- Setup---------------------------------------------------------* +*#########################################################################################################################*/ +static u32 color_pitch; +static u32 color_offset[2]; +static u32* color_buffer[2]; + +static u32 depth_pitch; +static u32 depth_offset; +static u32* depth_buffer; + +static void CreateContext(void) { + void* host_addr = memalign(1024 * 1024, HOST_SIZE); + rsxInit(&context, CB_SIZE, HOST_SIZE, host_addr); +} + +static void ConfigureVideo(void) { + videoState state; + videoGetState(0, 0, &state); + + videoConfiguration vconfig = { 0 }; + vconfig.resolution = state.displayMode.resolution; + vconfig.format = VIDEO_BUFFER_FORMAT_XRGB; + vconfig.pitch = DisplayInfo.Width * sizeof(u32); + + videoConfigure(0, &vconfig, NULL, 0); +} + +static void SetupBlendingState(void) { + rsxSetBlendFunc(context, GCM_SRC_ALPHA, GCM_ONE_MINUS_SRC_ALPHA, GCM_SRC_ALPHA, GCM_ONE_MINUS_SRC_ALPHA); + rsxSetBlendEquation(context, GCM_FUNC_ADD, GCM_FUNC_ADD); +} + +static void AllocColorSurface(u32 i) { + color_pitch = DisplayInfo.Width * 4; + color_buffer[i] = (u32*)rsxMemalign(64, DisplayInfo.Height * color_pitch); + + rsxAddressToOffset(color_buffer[i], &color_offset[i]); + gcmSetDisplayBuffer(i, color_offset[i], color_pitch, + DisplayInfo.Width, DisplayInfo.Height); +} + +static void AllocDepthSurface(void) { + depth_pitch = DisplayInfo.Width * 4; + depth_buffer = (u32*)rsxMemalign(64, DisplayInfo.Height * depth_pitch); + + rsxAddressToOffset(depth_buffer, &depth_offset); +} + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +void SetRenderTarget(u32 index) { + gcmSurface sf; + + sf.colorFormat = GCM_SURFACE_X8R8G8B8; + sf.colorTarget = GCM_SURFACE_TARGET_0; + sf.colorLocation[0] = GCM_LOCATION_RSX; + sf.colorOffset[0] = color_offset[index]; + sf.colorPitch[0] = color_pitch; + + sf.colorLocation[1] = GCM_LOCATION_RSX; + sf.colorLocation[2] = GCM_LOCATION_RSX; + sf.colorLocation[3] = GCM_LOCATION_RSX; + sf.colorOffset[1] = 0; + sf.colorOffset[2] = 0; + sf.colorOffset[3] = 0; + sf.colorPitch[1] = 64; + sf.colorPitch[2] = 64; + sf.colorPitch[3] = 64; + + sf.depthFormat = GCM_SURFACE_ZETA_Z24S8; + sf.depthLocation = GCM_LOCATION_RSX; + sf.depthOffset = depth_offset; + sf.depthPitch = depth_pitch; + + sf.type = GCM_SURFACE_TYPE_LINEAR; + sf.antiAlias = GCM_SURFACE_CENTER_1; + + sf.width = DisplayInfo.Width; + sf.height = DisplayInfo.Height; + sf.x = 0; + sf.y = 0; + + rsxSetSurface(context,&sf); +} + +static void InitGfxContext(void) { + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.Created = true; + + // https://github.com/ps3dev/PSL1GHT/blob/master/ppu/include/rsx/rsx.h#L30 + CreateContext(); + ConfigureVideo(); + gcmSetFlipMode(GCM_FLIP_VSYNC); + + AllocColorSurface(0); + AllocColorSurface(1); + AllocDepthSurface(); + gcmResetFlipStatus(); + + SetupBlendingState(); + SetRenderTarget(cur_fb); + + LoadVertexPrograms(); + LoadFragmentPrograms(); +} +static GfxResourceID white_square; + +void Gfx_Create(void) { + // TODO rethink all this + if (!Gfx.Created) InitGfxContext(); + + Gfx_RestoreState(); + gfx_format = -1; +} + +void Gfx_Free(void) { Gfx_FreeState(); } + + +cc_bool Gfx_TryRestoreContext(void) { return true; } +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 1x1 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + + +u32* Gfx_AllocImage(u32* offset, s32 w, s32 h) { + u32* pixels = (u32*)rsxMemalign(64, w * h * 4); + rsxAddressToOffset(pixels, offset); + return pixels; +} + +void Gfx_TransferImage(u32 offset, s32 w, s32 h) { + rsxSetTransferImage(context, GCM_TRANSFER_LOCAL_TO_LOCAL, + color_offset[cur_fb], color_pitch, 0, 0, + offset, w * 4, 0, 0, + w, h, 4); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint32 clearColor; + +void Gfx_SetFaceCulling(cc_bool enabled) { + rsxSetCullFaceEnable(context, enabled); +} + +static void SetAlphaBlend(cc_bool enabled) { + rsxSetBlendEnable(context, enabled); +} +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_ClearColor(PackedCol color) { + cc_uint32 R = PackedCol_R(color); + cc_uint32 G = PackedCol_G(color); + cc_uint32 B = PackedCol_B(color); + + clearColor = B | (G << 8) | (R << 16) | (0xFF << 24); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + rsxSetDepthWriteEnable(context, enabled); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + rsxSetDepthTestEnable(context, enabled); +} + +static void SetAlphaTest(cc_bool enabled) { + rsxSetAlphaTestEnable(context, enabled); +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + unsigned mask = 0; + if (r) mask |= GCM_COLOR_MASK_R; + if (g) mask |= GCM_COLOR_MASK_G; + if (b) mask |= GCM_COLOR_MASK_B; + if (a) mask |= GCM_COLOR_MASK_A; + + rsxSetColorMask(context, mask); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // Same as Direct3D9 + // TODO: should it be like OpenGL? ??? + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + // Same as Direct3D9 + // TODO: should it be like OpenGL? ??? + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zNear - zFar); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + int pointerSize = sizeof(void*) * 8; + + String_Format1(info, "-- Using PS3 (%i bit) --\n", &pointerSize); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +static void ResetFrameState(void) { + // Seems these states aren't persisted across frames + // TODO: Why is that + rsxSetAlphaFunc(context, GCM_GREATER, 0.5f); + rsxSetBlendFunc(context, GCM_SRC_ALPHA, GCM_ONE_MINUS_SRC_ALPHA, GCM_SRC_ALPHA, GCM_ONE_MINUS_SRC_ALPHA); + rsxSetBlendEquation(context, GCM_FUNC_ADD, GCM_FUNC_ADD); + + rsxSetColorMaskMrt(context, 0); + rsxSetClearColor(context, clearColor); + rsxSetClearDepthStencil(context, 0xFFFFFFFF); + + rsxSetDepthFunc(context, GCM_LEQUAL); + rsxSetDepthWriteEnable(context, true); + rsxSetDepthTestEnable(context, true); + + rsxSetUserClipPlaneControl(context, GCM_USER_CLIP_PLANE_DISABLE, + GCM_USER_CLIP_PLANE_DISABLE, + GCM_USER_CLIP_PLANE_DISABLE, + GCM_USER_CLIP_PLANE_DISABLE, + GCM_USER_CLIP_PLANE_DISABLE, + GCM_USER_CLIP_PLANE_DISABLE); + + // NOTE: Must be called each frame, otherwise renders upside down at 4x zoom + Gfx_SetViewport(0, 0, Game.Width, Game.Height); +} + +// https://github.com/ps3dev/PSL1GHT/blob/master/ppu/include/rsx/rsx.h#L30 +static cc_bool everFlipped; +void Gfx_BeginFrame(void) { + // TODO: remove everFlipped + if (everFlipped) { + while (gcmGetFlipStatus() != 0) usleep(200); + } + + ResetFrameState(); + everFlipped = true; + gcmResetFlipStatus(); +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + int targets = 0; + if (buffers & GFX_BUFFER_COLOR) targets |= (GCM_CLEAR_R | GCM_CLEAR_G | GCM_CLEAR_B | GCM_CLEAR_A); + if (buffers & GFX_BUFFER_DEPTH) targets |= (GCM_CLEAR_S | GCM_CLEAR_Z); + + rsxClearSurface(context, targets); +} + +void Gfx_EndFrame(void) { + gcmSetFlip(context, cur_fb); + rsxFlushBuffer(context); + gcmSetWaitFlip(context); + + cur_fb ^= 1; + SetRenderTarget(cur_fb); + + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_OnWindowResize(void) { + Gfx_SetViewport(0, 0, Game.Width, Game.Height); +} + +void Gfx_SetViewport(int x, int y, int w, int h) { + f32 scale[4], offset[4]; + f32 zmin = 0.0f; + f32 zmax = 1.0f; + y = Game.Height - y - h; + + scale[0] = w * 0.5f; + scale[1] = h * -0.5f; + scale[2] = (zmax - zmin) * 0.5f; + scale[3] = 0.0f; + offset[0] = x + w * 0.5f; + offset[1] = x + h * 0.5f; + offset[2] = (zmax + zmin) * 0.5f; + offset[3] = 0.0f; + + rsxSetViewport(context, x, y, w, h, zmin, zmax, scale, offset); + rsxSetScissor(context, x, y, w, h); + + // TODO: even needed? + for (int i = 0; i < 8; i++) + { + rsxSetViewportClip(context, i, w, h); + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static int vb_size; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return 1;/* TODO */ +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return rsxMemalign(128, count * strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { + u32 offset; + rsxAddressToOffset(vb, &offset); + + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + rsxBindVertexArrayAttrib(context, GCM_VERTEX_ATTRIB_POS, 0, offset, + SIZEOF_VERTEX_TEXTURED, 3, GCM_VERTEX_DATA_TYPE_F32, GCM_LOCATION_RSX); + rsxBindVertexArrayAttrib(context, GCM_VERTEX_ATTRIB_COLOR0, 0, offset + 12, + SIZEOF_VERTEX_TEXTURED, 4, GCM_VERTEX_DATA_TYPE_U8, GCM_LOCATION_RSX); + rsxBindVertexArrayAttrib(context, GCM_VERTEX_ATTRIB_TEX0, 0, offset + 16, + SIZEOF_VERTEX_TEXTURED, 2, GCM_VERTEX_DATA_TYPE_F32, GCM_LOCATION_RSX); + } else { + rsxBindVertexArrayAttrib(context, GCM_VERTEX_ATTRIB_POS, 0, offset, + SIZEOF_VERTEX_COLOURED, 3, GCM_VERTEX_DATA_TYPE_F32, GCM_LOCATION_RSX); + rsxBindVertexArrayAttrib(context, GCM_VERTEX_ATTRIB_COLOR0, 0, offset + 12, + SIZEOF_VERTEX_COLOURED, 4, GCM_VERTEX_DATA_TYPE_U8, GCM_LOCATION_RSX); + } +} + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb;/* TODO */ + if (data) rsxFree(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + Gfx_BindVb(vb); + rsxInvalidateVertexCache(context); // TODO needed? +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return rsxMemalign(128, maxVertices * strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + Gfx_BindVb(vb); + rsxInvalidateVertexCache(context); // TODO needed? +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCTexture_ { + cc_uint32 width, height; + cc_uint32 pad[(128 - 8)/4]; // TODO better way of aligning to 128 bytes + cc_uint32 pixels[]; +} CCTexture; + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + int size = bmp->width * bmp->height * 4; + CCTexture* tex = (CCTexture*)rsxMemalign(128, 128 + size); + + tex->width = bmp->width; + tex->height = bmp->height; + CopyTextureData(tex->pixels, bmp->width * 4, bmp, rowWidth << 2); + return tex; +} + +void Gfx_BindTexture(GfxResourceID texId) { + CCTexture* tex = (CCTexture*)texId; + if (!tex) tex = white_square; + /* TODO */ + + u32 offset; + rsxAddressToOffset(tex->pixels, &offset); + gcmTexture texture; + + texture.format = GCM_TEXTURE_FORMAT_A8R8G8B8 | GCM_TEXTURE_FORMAT_LIN; + texture.mipmap = 1; + texture.dimension = GCM_TEXTURE_DIMS_2D; + texture.cubemap = GCM_FALSE; + texture.remap = ((GCM_TEXTURE_REMAP_TYPE_REMAP << GCM_TEXTURE_REMAP_TYPE_B_SHIFT) | + (GCM_TEXTURE_REMAP_TYPE_REMAP << GCM_TEXTURE_REMAP_TYPE_G_SHIFT) | + (GCM_TEXTURE_REMAP_TYPE_REMAP << GCM_TEXTURE_REMAP_TYPE_R_SHIFT) | + (GCM_TEXTURE_REMAP_TYPE_REMAP << GCM_TEXTURE_REMAP_TYPE_A_SHIFT) | + (GCM_TEXTURE_REMAP_COLOR_B << GCM_TEXTURE_REMAP_COLOR_B_SHIFT) | + (GCM_TEXTURE_REMAP_COLOR_G << GCM_TEXTURE_REMAP_COLOR_G_SHIFT) | + (GCM_TEXTURE_REMAP_COLOR_R << GCM_TEXTURE_REMAP_COLOR_R_SHIFT) | + (GCM_TEXTURE_REMAP_COLOR_A << GCM_TEXTURE_REMAP_COLOR_A_SHIFT)); + texture.width = tex->width; + texture.height = tex->height; + texture.depth = 1; + texture.location = GCM_LOCATION_RSX; + texture.pitch = tex->width * 4; + texture.offset = offset; + + rsxInvalidateTextureCache(context,GCM_INVALIDATE_TEXTURE); // TODO needed + + rsxLoadTexture(context, 0, &texture); + rsxTextureControl(context, 0, GCM_TRUE, 0<<8, 12<<8, GCM_TEXTURE_MAX_ANISO_1); + rsxTextureFilter(context, 0, 0, GCM_TEXTURE_NEAREST, GCM_TEXTURE_NEAREST, + GCM_TEXTURE_CONVOLUTION_QUINCUNX); + rsxTextureWrapMode(context, 0, GCM_TEXTURE_REPEAT, GCM_TEXTURE_REPEAT, GCM_TEXTURE_REPEAT, 0, GCM_TEXTURE_ZFUNC_LESS, 0); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GfxResourceID data = *texId; + if (data) rsxFree(data); + *texId = NULL; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)texId; + + // NOTE: Only valid for LINEAR textures + cc_uint32* dst = (tex->pixels + x) + y * tex->width; + CopyTextureData(dst, tex->width * 4, part, rowWidth << 2); + + rsxInvalidateTextureCache(context, GCM_INVALIDATE_TEXTURE); + /* TODO */ +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) {/* TODO */ +} + +void Gfx_SetFogCol(PackedCol color) {/* TODO */ +} + +void Gfx_SetFogDensity(float value) {/* TODO */ +} + +void Gfx_SetFogEnd(float value) {/* TODO */ +} + +void Gfx_SetFogMode(FogFunc func) {/* TODO */ +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + struct Matrix* dst = type == MATRIX_PROJECTION ? &_proj : &_view; + *dst = *matrix; + + Matrix_Mul(&mvp, &_view, &_proj); + VP_UpdateUniforms(); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + textureOffseting = true; + textureOffset[0] = x; + textureOffset[1] = y; + + VP_SwitchActive(); + VP_UpdateUniforms(); +} + +void Gfx_DisableTextureOffset(void) { + textureOffseting = false; + VP_SwitchActive(); +} + + +/*########################################################################################################################* +*----------------------------------------------------------Drawing--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt];/* TODO */ + + VP_SwitchActive(); + FP_SwitchActive(); +} + +void Gfx_DrawVb_Lines(int verticesCount) {/* TODO */ + rsxDrawVertexArray(context, GCM_TYPE_LINES, 0, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) {/* TODO */ + rsxDrawVertexArray(context, GCM_TYPE_QUADS, startVertex, verticesCount); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) {/* TODO */ + rsxDrawVertexArray(context, GCM_TYPE_QUADS, 0, verticesCount); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) {/* TODO */ + rsxDrawVertexArray(context, GCM_TYPE_QUADS, startVertex, verticesCount); +} +#endif diff --git a/src/Graphics_PSP.c b/src/Graphics_PSP.c new file mode 100644 index 0000000..75cedff --- /dev/null +++ b/src/Graphics_PSP.c @@ -0,0 +1,450 @@ +#include "Core.h" +#if defined CC_BUILD_PSP +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" + +#include +#include +#include +#include +#include +#include + +#define BUFFER_WIDTH 512 +#define SCREEN_WIDTH 480 +#define SCREEN_HEIGHT 272 + +#define FB_SIZE (BUFFER_WIDTH * SCREEN_HEIGHT * 4) +static unsigned int __attribute__((aligned(16))) list[262144]; + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static int formatFields[] = { + GU_TEXTURE_32BITF | GU_VERTEX_32BITF | GU_TRANSFORM_3D, + GU_TEXTURE_32BITF | GU_COLOR_8888 | GU_VERTEX_32BITF | GU_TRANSFORM_3D +}; +static ScePspFMatrix4 identity; + +static void guInit(void) { + void* framebuffer0 = (void*)0; + void* framebuffer1 = (void*)FB_SIZE; + void* depthbuffer = (void*)(FB_SIZE + FB_SIZE); + Mem_Copy(&identity, &Matrix_Identity, sizeof(ScePspFMatrix4)); + + sceGuInit(); + sceGuStart(GU_DIRECT, list); + sceGuDrawBuffer(GU_PSM_8888, framebuffer0, BUFFER_WIDTH); + sceGuDispBuffer(SCREEN_WIDTH, SCREEN_HEIGHT, framebuffer1, BUFFER_WIDTH); + sceGuDepthBuffer(depthbuffer, BUFFER_WIDTH); + + sceGuOffset(2048 - (SCREEN_WIDTH / 2), 2048 - (SCREEN_HEIGHT / 2)); + sceGuViewport(2048, 2048, SCREEN_WIDTH, SCREEN_HEIGHT); + sceGuDepthRange(65535,0); + sceGuFrontFace(GU_CCW); + sceGuShadeModel(GU_SMOOTH); + sceGuDisable(GU_TEXTURE_2D); + + sceGuAlphaFunc(GU_GREATER, 0x7f, 0xff); + sceGuBlendFunc(GU_ADD, GU_SRC_ALPHA, GU_ONE_MINUS_SRC_ALPHA, 0, 0); + sceGuDepthFunc(GU_LEQUAL); // sceGuDepthFunc(GU_GEQUAL); + sceGuClearDepth(65535); // sceGuClearDepth(0); + sceGuDepthRange(0, 65535); // sceGuDepthRange(65535, 0); + + + sceGuSetMatrix(GU_MODEL, &identity); + sceGuColor(0xffffffff); + sceGuScissor(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + sceGuDisable(GU_SCISSOR_TEST); + + sceGuEnable(GU_CLIP_PLANES); // TODO: swap near/far instead of this? + sceGuTexFunc(GU_TFX_MODULATE, GU_TCC_RGBA); + + sceGuFinish(); + sceGuSync(0,0); + sceDisplayWaitVblankStart(); + sceGuDisplay(GU_TRUE); +} + +static GfxResourceID white_square; +void Gfx_Create(void) { + if (!Gfx.Created) guInit(); + + Gfx.MaxTexWidth = 512; + Gfx.MaxTexHeight = 512; + Gfx.Created = true; + gfx_vsync = true; + + Gfx_RestoreState(); +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 1x1 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +#define GU_Toggle(cap) if (enabled) { sceGuEnable(cap); } else { sceGuDisable(cap); } + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCTexture_ { + cc_uint32 width, height; + cc_uint32 pad1, pad2; // data must be aligned to 16 bytes + cc_uint32 pixels[]; +} CCTexture; + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + int size = bmp->width * bmp->height * 4; + CCTexture* tex = (CCTexture*)memalign(16, 16 + size); + + tex->width = bmp->width; + tex->height = bmp->height; + CopyTextureData(tex->pixels, bmp->width * 4, bmp, rowWidth << 2); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)texId; + cc_uint32* dst = (tex->pixels + x) + y * tex->width; + CopyTextureData(dst, tex->width * 4, part, rowWidth << 2); + // TODO: Do line by line and only invalidate the actually changed parts of lines? + sceKernelDcacheWritebackInvalidateRange(dst, (tex->width * part->height) * 4); +} + +/*void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)texId; + cc_uint32* dst = (tex->pixels + x) + y * tex->width; + + for (int yy = 0; yy < part->height; yy++) { + Mem_Copy(dst + (y + yy) * tex->width, part->scan0 + yy * rowWidth, part->width * 4); + } +}*/ + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GfxResourceID data = *texId; + if (data) Mem_Free(data); + *texId = NULL; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + +void Gfx_BindTexture(GfxResourceID texId) { + CCTexture* tex = (CCTexture*)texId; + if (!tex) tex = white_square; + + sceGuTexMode(GU_PSM_8888,0,0,0); + sceGuTexImage(0, tex->width, tex->height, tex->width, tex->pixels); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_clearColor; +void Gfx_SetFaceCulling(cc_bool enabled) { GU_Toggle(GU_CULL_FACE); } +static void SetAlphaBlend(cc_bool enabled) { GU_Toggle(GU_BLEND); } +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_ClearColor(PackedCol color) { + if (color == gfx_clearColor) return; + sceGuClearColor(color); + gfx_clearColor = color; +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + unsigned int mask = 0xffffffff; + if (r) mask &= 0xffffff00; + if (g) mask &= 0xffff00ff; + if (b) mask &= 0xff00ffff; + if (a) mask &= 0x00ffffff; + + sceGuPixelMask(mask); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + sceGuDepthMask(enabled ? 0 : 0xffffffff); +} +void Gfx_SetDepthTest(cc_bool enabled) { GU_Toggle(GU_DEPTH_TEST); } + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho + // The simplified calculation below uses: L = 0, R = width, T = 0, B = height + // NOTE: Shared with OpenGL. might be wrong to do that though? + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -2.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -(zFar + zNear) / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + // Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum + // For a FOV based perspective matrix, left/right/top/bottom are calculated as: + // left = -c * aspect, right = c * aspect, bottom = -c, top = c + // Calculations are simplified because of left/right and top/bottom symmetry + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; + // TODO: should direct3d9 one be used insted with clip range from 0,1 ? +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +static BitmapCol* PSP_GetRow(struct Bitmap* bmp, int y, void* ctx) { + cc_uint8* fb = (cc_uint8*)ctx; + return (BitmapCol*)(fb + y * BUFFER_WIDTH * 4); +} + +cc_result Gfx_TakeScreenshot(struct Stream* output) { + int fbWidth, fbFormat; + void* fb; + + int res = sceDisplayGetFrameBuf(&fb, &fbWidth, &fbFormat, PSP_DISPLAY_SETBUF_NEXTFRAME); + if (res < 0) return res; + if (!fb) return ERR_NOT_SUPPORTED; + + struct Bitmap bmp; + bmp.scan0 = NULL; + bmp.width = SCREEN_WIDTH; + bmp.height = SCREEN_HEIGHT; + + return Png_Encode(&bmp, output, PSP_GetRow, false, fb); +} + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using PSP--\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { + sceGuStart(GU_DIRECT, list); +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + int targets = 0; + if (buffers & GFX_BUFFER_COLOR) targets |= GU_COLOR_BUFFER_BIT; + if (buffers & GFX_BUFFER_DEPTH) targets |= GU_DEPTH_BUFFER_BIT; + + sceGuClear(targets); +} + +void Gfx_EndFrame(void) { + sceGuFinish(); + sceGuSync(0, 0); + + if (gfx_vsync) sceDisplayWaitVblankStart(); + sceGuSwapBuffers(); + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_OnWindowResize(void) { } + +void Gfx_SetViewport(int x, int y, int w, int h) { } + + +static cc_uint8* gfx_vertices; +static int gfx_fields; + + +/*########################################################################################################################* +*----------------------------------------------------------Buffers--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint16 __attribute__((aligned(16))) gfx_indices[GFX_MAX_INDICES]; +static int vb_size; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + fillFunc(gfx_indices, count, obj); +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return memalign(16, count * strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; + sceKernelDcacheWritebackInvalidateRange(vb, vb_size); +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return memalign(16, maxVertices * strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + vb_size = count * strideSizes[fmt]; + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; + sceKernelDcacheWritebackInvalidateRange(vb, vb_size); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol gfx_fogColor; +static float gfx_fogEnd = -1.0f, gfx_fogDensity = -1.0f; +static int gfx_fogMode = -1; + +void Gfx_SetFog(cc_bool enabled) { + gfx_fogEnabled = enabled; + //GU_Toggle(GU_FOG); +} + +void Gfx_SetFogCol(PackedCol color) { + if (color == gfx_fogColor) return; + gfx_fogColor = color; + //sceGuFog(0.0f, gfx_fogEnd, gfx_fogColor); +} + +void Gfx_SetFogDensity(float value) { +} + +void Gfx_SetFogEnd(float value) { + if (value == gfx_fogEnd) return; + gfx_fogEnd = value; + //sceGuFog(0.0f, gfx_fogEnd, gfx_fogColor); +} + +void Gfx_SetFogMode(FogFunc func) { + /* TODO: Implemen fake exp/exp2 fog */ +} + +static void SetAlphaTest(cc_bool enabled) { GU_Toggle(GU_ALPHA_TEST); } + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static int matrix_modes[] = { GU_PROJECTION, GU_VIEW }; +static ScePspFMatrix4 tmp_matrix; // 16 byte aligned + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + Mem_Copy(&tmp_matrix, matrix, sizeof(ScePspFMatrix4)); + sceGuSetMatrix(matrix_modes[type], &tmp_matrix); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + sceGuSetMatrix(matrix_modes[type], &identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + sceGuTexOffset(x, y); +} + +void Gfx_DisableTextureOffset(void) { + sceGuTexOffset(0.0f, 0.0f); +} + + + +/*########################################################################################################################* +*---------------------------------------------------------Drawing---------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_fields = formatFields[fmt]; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_TEXTURED) { + sceGuEnable(GU_TEXTURE_2D); + } else { + sceGuDisable(GU_TEXTURE_2D); + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { + sceGuDrawArray(GU_LINES, gfx_fields, verticesCount, NULL, gfx_vertices); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + sceGuDrawArray(GU_TRIANGLES, gfx_fields | GU_INDEX_16BIT, ICOUNT(verticesCount), + gfx_indices, gfx_vertices + startVertex * gfx_stride); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + sceGuDrawArray(GU_TRIANGLES, gfx_fields | GU_INDEX_16BIT, ICOUNT(verticesCount), + gfx_indices, gfx_vertices); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + sceGuDrawArray(GU_TRIANGLES, gfx_fields | GU_INDEX_16BIT, ICOUNT(verticesCount), + gfx_indices, gfx_vertices + startVertex * SIZEOF_VERTEX_TEXTURED); +} +#endif diff --git a/src/Graphics_PSVita.c b/src/Graphics_PSVita.c new file mode 100644 index 0000000..39a9d59 --- /dev/null +++ b/src/Graphics_PSVita.c @@ -0,0 +1,1113 @@ +#include "Core.h" +#if defined CC_BUILD_PSVITA +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include + +// TODO track last frame used on +static cc_bool gfx_depthOnly; +static int frontBufferIndex, backBufferIndex; +// Inspired from +// https://github.com/xerpi/gxmfun/blob/master/source/main.c +// https://github.com/vitasdk/samples/blob/6337766482561cf28092d21082202c0f01e3542b/gxm/textured_cube/src/main.c + +#define DISPLAY_WIDTH 960 +#define DISPLAY_HEIGHT 544 +#define DISPLAY_STRIDE 1024 + +#define NUM_DISPLAY_BUFFERS 3 // TODO: or just 2? +#define MAX_PENDING_SWAPS (NUM_DISPLAY_BUFFERS - 1) + +static void GPUBuffers_DeleteUnreferenced(void); +static void GPUTextures_DeleteUnreferenced(void); +static cc_uint32 frameCounter; +static cc_bool in_scene; + +static SceGxmContext* gxm_context; + +static SceUID vdm_ring_buffer_uid; +static void* vdm_ring_buffer_addr; +static SceUID vertex_ring_buffer_uid; +static void* vertex_ring_buffer_addr; +static SceUID fragment_ring_buffer_uid; +static void* fragment_ring_buffer_addr; +static SceUID fragment_usse_ring_buffer_uid; +static void* fragment_usse_ring_buffer_addr; +static unsigned int fragment_usse_offset; + +static SceGxmRenderTarget* gxm_render_target; +static SceGxmColorSurface gxm_color_surfaces[NUM_DISPLAY_BUFFERS]; +static SceUID gxm_color_surfaces_uid[NUM_DISPLAY_BUFFERS]; +static void* gxm_color_surfaces_addr[NUM_DISPLAY_BUFFERS]; +static SceGxmSyncObject* gxm_sync_objects[NUM_DISPLAY_BUFFERS]; + +static SceUID gxm_depth_stencil_surface_uid; +static void* gxm_depth_stencil_surface_addr; +static SceGxmDepthStencilSurface gxm_depth_stencil_surface; + +static SceGxmShaderPatcher *gxm_shader_patcher; +static const int shader_patcher_buffer_size = 64 * 1024; +static SceUID gxm_shader_patcher_buffer_uid; +static void* gxm_shader_patcher_buffer_addr; + +static const int shader_patcher_vertex_usse_size = 64 * 1024; +static SceUID gxm_shader_patcher_vertex_usse_uid; +static void* gxm_shader_patcher_vertex_usse_addr; +static unsigned int shader_patcher_vertex_usse_offset; + +static const int shader_patcher_fragment_usse_size = 64 * 1024; +static SceUID gxm_shader_patcher_fragment_usse_uid; +static void* gxm_shader_patcher_fragment_usse_addr; +static unsigned int shader_patcher_fragment_usse_offset; + + +#include "../misc/vita/colored_f.h" +#include "../misc/vita/colored_v.h" +static SceGxmProgram* gxm_colored_VP = (SceGxmProgram *)&colored_v; +static SceGxmProgram* gxm_colored_FP = (SceGxmProgram *)&colored_f; + +#include "../misc/vita/textured_f.h" +#include "../misc/vita/textured_v.h" +static SceGxmProgram* gxm_textured_VP = (SceGxmProgram *)&textured_v; +static SceGxmProgram* gxm_textured_FP = (SceGxmProgram *)&textured_f; + +#include "../misc/vita/colored_alpha_f.h" +static SceGxmProgram* gxm_colored_alpha_FP = (SceGxmProgram *)&colored_alpha_f; +#include "../misc/vita/textured_alpha_f.h" +static SceGxmProgram* gxm_textured_alpha_FP = (SceGxmProgram *)&textured_alpha_f; + + +typedef struct CCVertexProgram { + SceGxmVertexProgram* programPatched; + const SceGxmProgramParameter* param_in_position; + const SceGxmProgramParameter* param_in_color; + const SceGxmProgramParameter* param_in_texcoord; + const SceGxmProgramParameter* param_uni_mvp; + int dirtyUniforms; +} VertexProgram; +#define VP_UNI_MATRIX 0x01 + +typedef struct CCFragmentProgram { + SceGxmFragmentProgram* programPatched; +} FragmentProgram; + + +/*########################################################################################################################* +*---------------------------------------------------------Memory----------------------------------------------------------* +*#########################################################################################################################*/ +#define ALIGNUP(size, a) (((size) + ((a) - 1)) & ~((a) - 1)) + +void* AllocGPUMemory(int size, int type, int gpu_access, SceUID* ret_uid) { + SceUID uid; + void* addr; + + if (type == SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW) { + size = ALIGNUP(size, 256 * 1024); + } else { + size = ALIGNUP(size, 4 * 1024); + } + + // https://wiki.henkaku.xyz/vita/SceSysmem + uid = sceKernelAllocMemBlock("GPU memory", type, size, NULL); + if (uid < 0) Logger_Abort2(uid, "Failed to allocate GPU memory block"); + + int res1 = sceKernelGetMemBlockBase(uid, &addr); + if (res1 < 0) Logger_Abort2(res1, "Failed to get base of GPU memory block"); + + int res2 = sceGxmMapMemory(addr, size, gpu_access); + if (res1 < 0) Logger_Abort2(res2, "Failed to map memory for GPU usage"); + // https://wiki.henkaku.xyz/vita/GPU + + *ret_uid = uid; + return addr; +} + +void* AllocGPUVertexUSSE(size_t size, SceUID* ret_uid, unsigned int* ret_usse_offset) { + SceUID uid; + void *addr; + + size = ALIGNUP(size, 4 * 1024); + + uid = sceKernelAllocMemBlock("GPU vertex USSE", + SCE_KERNEL_MEMBLOCK_TYPE_USER_RW_UNCACHE, size, NULL); + if (uid < 0) Logger_Abort2(uid, "Failed to allocate vertex USSE block"); + + int res1 = sceKernelGetMemBlockBase(uid, &addr); + if (res1 < 0) Logger_Abort2(res1, "Failed to get base of vertex USSE memory block"); + + int res2 = sceGxmMapVertexUsseMemory(addr, size, ret_usse_offset); + if (res1 < 0) Logger_Abort2(res2, "Failed to map vertex USSE memory"); + + *ret_uid = uid; + return addr; +} + +void* AllocGPUFragmentUSSE(size_t size, SceUID* ret_uid, unsigned int* ret_usse_offset) { + SceUID uid; + void *addr; + + size = ALIGNUP(size, 4 * 1024); + + uid = sceKernelAllocMemBlock("GPU fragment USSE", + SCE_KERNEL_MEMBLOCK_TYPE_USER_RW_UNCACHE, size, NULL); + if (uid < 0) Logger_Abort2(uid, "Failed to allocate fragment USSE block"); + + int res1 = sceKernelGetMemBlockBase(uid, &addr); + if (res1 < 0) Logger_Abort2(res1, "Failed to get base of fragment USSE memory block"); + + int res2 = sceGxmMapFragmentUsseMemory(addr, size, ret_usse_offset); + if (res1 < 0) Logger_Abort2(res2, "Failed to map fragment USSE memory"); + + *ret_uid = uid; + return addr; +} + +static void FreeGPUMemory(SceUID uid) { + void *addr; + + if (sceKernelGetMemBlockBase(uid, &addr) < 0) + return; + + sceGxmUnmapMemory(addr); + sceKernelFreeMemBlock(uid); +} + +static void* AllocShaderPatcherMem(void* user_data, unsigned int size) { + return Mem_TryAlloc(1, size); +} + +static void FreeShaderPatcherMem(void* user_data, void* mem) { + Mem_Free(mem); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Vertex shaders------------------------------------------------------* +*#########################################################################################################################*/ +static VertexProgram VP_list[2]; +static VertexProgram* VP_Active; +static float transposed_mvp[4*4] __attribute__((aligned(64))); + +static void VP_DirtyUniform(int uniform) { + for (int i = 0; i < Array_Elems(VP_list); i++) + { + VP_list[i].dirtyUniforms |= uniform; + } +} + +static void VP_ReloadUniforms(void) { + VertexProgram* VP = VP_Active; + // Calling sceGxmReserveVertexDefaultUniformBuffer when not in a scene + // results in SCE_GXM_ERROR_NOT_WITHIN_SCENE on real hardware + if (!VP || !in_scene) return; + int ret; + + if (VP->dirtyUniforms & VP_UNI_MATRIX) { + void *uniform_buffer = NULL; + + ret = sceGxmReserveVertexDefaultUniformBuffer(gxm_context, &uniform_buffer); + if (ret) Logger_Abort2(ret, "Reserving uniform buffer"); + ret = sceGxmSetUniformDataF(uniform_buffer, VP->param_uni_mvp, 0, 4 * 4, transposed_mvp); + if (ret) Logger_Abort2(ret, "Updating uniform buffer"); + + VP->dirtyUniforms &= ~VP_UNI_MATRIX; + } +} + +static void VP_SwitchActive(void) { + int index = gfx_format == VERTEX_FORMAT_TEXTURED ? 1 : 0; + + VertexProgram* VP = &VP_list[index]; + if (VP == VP_Active) return; + VP_Active = VP; + + VP->dirtyUniforms |= VP_UNI_MATRIX; // Need to update uniforms after switching program + sceGxmSetVertexProgram(gxm_context, VP->programPatched); + VP_ReloadUniforms(); +} + + +/*########################################################################################################################* +*----------------------------------------------------Fragment shaders-----------------------------------------------------* +*#########################################################################################################################*/ +static FragmentProgram FP_list[4 * 3]; +static FragmentProgram* FP_Active; + +static void FP_SwitchActive(void) { + int index = gfx_format == VERTEX_FORMAT_TEXTURED ? 3 : 0; + + // [normal rendering, blend rendering, no rendering] + if (gfx_depthOnly) { + index += 2; + } else if (gfx_alphaBlend) { + index += 1; + } + + if (gfx_alphaTest) index += 2 * 3; + + FragmentProgram* FP = &FP_list[index]; + if (FP == FP_Active) return; + FP_Active = FP; + + sceGxmSetFragmentProgram(gxm_context, FP->programPatched); +} + + +static const SceGxmBlendInfo no_blending = { + SCE_GXM_COLOR_MASK_ALL, + SCE_GXM_BLEND_FUNC_NONE, SCE_GXM_BLEND_FUNC_NONE, + SCE_GXM_BLEND_FACTOR_ONE, SCE_GXM_BLEND_FACTOR_ZERO, + SCE_GXM_BLEND_FACTOR_ONE, SCE_GXM_BLEND_FACTOR_ZERO +}; +static const SceGxmBlendInfo yes_blending = { + SCE_GXM_COLOR_MASK_ALL, + SCE_GXM_BLEND_FUNC_ADD, SCE_GXM_BLEND_FUNC_ADD, + SCE_GXM_BLEND_FACTOR_SRC_ALPHA, SCE_GXM_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + SCE_GXM_BLEND_FACTOR_SRC_ALPHA, SCE_GXM_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA +}; +static const SceGxmBlendInfo no_rendering = { + SCE_GXM_COLOR_MASK_NONE, + SCE_GXM_BLEND_FUNC_NONE, SCE_GXM_BLEND_FUNC_NONE, + SCE_GXM_BLEND_FACTOR_ONE, SCE_GXM_BLEND_FACTOR_ZERO, + SCE_GXM_BLEND_FACTOR_ONE, SCE_GXM_BLEND_FACTOR_ZERO +}; +static const SceGxmBlendInfo* blend_modes[] = { &no_blending, &yes_blending, &no_rendering }; + +static void CreateFragmentPrograms(int index, const SceGxmProgram* fragProgram, const SceGxmProgram* vertexProgram) { + SceGxmShaderPatcherId programID; + + for (int i = 0; i < Array_Elems(blend_modes); i++) + { + FragmentProgram* FP = &FP_list[index + i]; + sceGxmShaderPatcherRegisterProgram(gxm_shader_patcher, fragProgram, &programID); + + const SceGxmProgram* prog = sceGxmShaderPatcherGetProgramFromId(programID); // TODO just use original program directly? + + sceGxmShaderPatcherCreateFragmentProgram(gxm_shader_patcher, + programID, SCE_GXM_OUTPUT_REGISTER_FORMAT_UCHAR4, + SCE_GXM_MULTISAMPLE_NONE, blend_modes[i], vertexProgram, + &FP->programPatched); + } +} + + +/*########################################################################################################################* +*-----------------------------------------------------Initialisation------------------------------------------------------* +*#########################################################################################################################*/ +struct DQCallbackData { void* addr; }; +void (*DQ_OnNextFrame)(void* fb); + +static void DQ_OnNextFrame3D(void* fb) { + if (gfx_vsync) sceDisplayWaitVblankStart(); + + GPUBuffers_DeleteUnreferenced(); + GPUTextures_DeleteUnreferenced(); + frameCounter++; +} + +static void DQCallback(const void *callback_data) { + SceDisplayFrameBuf fb = { 0 }; + + fb.size = sizeof(SceDisplayFrameBuf); + fb.base = ((struct DQCallbackData*)callback_data)->addr; + fb.pitch = DISPLAY_STRIDE; + fb.pixelformat = SCE_DISPLAY_PIXELFORMAT_A8B8G8R8; + fb.width = DISPLAY_WIDTH; + fb.height = DISPLAY_HEIGHT; + + sceDisplaySetFrameBuf(&fb, SCE_DISPLAY_SETBUF_NEXTFRAME); + DQ_OnNextFrame(fb.base); +} + +void Gfx_InitGXM(void) { // called from Window_Init + SceGxmInitializeParams params = { 0 }; + + params.displayQueueMaxPendingCount = MAX_PENDING_SWAPS; + params.displayQueueCallback = DQCallback; + params.displayQueueCallbackDataSize = sizeof(struct DQCallbackData); + params.parameterBufferSize = SCE_GXM_DEFAULT_PARAMETER_BUFFER_SIZE; + + sceGxmInitialize(¶ms); +} + +static void AllocRingBuffers(void) { + vdm_ring_buffer_addr = AllocGPUMemory(SCE_GXM_DEFAULT_VDM_RING_BUFFER_SIZE, + SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, SCE_GXM_MEMORY_ATTRIB_READ, + &vdm_ring_buffer_uid); + + vertex_ring_buffer_addr = AllocGPUMemory(SCE_GXM_DEFAULT_VERTEX_RING_BUFFER_SIZE, + SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, SCE_GXM_MEMORY_ATTRIB_READ, + &vertex_ring_buffer_uid); + + fragment_ring_buffer_addr = AllocGPUMemory(SCE_GXM_DEFAULT_FRAGMENT_RING_BUFFER_SIZE, + SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, SCE_GXM_MEMORY_ATTRIB_READ, + &fragment_ring_buffer_uid); + + fragment_usse_ring_buffer_addr = AllocGPUFragmentUSSE(SCE_GXM_DEFAULT_FRAGMENT_USSE_RING_BUFFER_SIZE, + &fragment_ring_buffer_uid, &fragment_usse_offset); +} + +static void AllocGXMContext(void) { + SceGxmContextParams params = { 0 }; + + params.hostMem = Mem_Alloc(1, SCE_GXM_MINIMUM_CONTEXT_HOST_MEM_SIZE, "Host memory"); + params.hostMemSize = SCE_GXM_MINIMUM_CONTEXT_HOST_MEM_SIZE; + + params.vdmRingBufferMem = vdm_ring_buffer_addr; + params.vdmRingBufferMemSize = SCE_GXM_DEFAULT_VDM_RING_BUFFER_SIZE; + + params.vertexRingBufferMem = vertex_ring_buffer_addr; + params.vertexRingBufferMemSize = SCE_GXM_DEFAULT_VERTEX_RING_BUFFER_SIZE; + + params.fragmentRingBufferMem = fragment_ring_buffer_addr; + params.fragmentRingBufferMemSize = SCE_GXM_DEFAULT_FRAGMENT_RING_BUFFER_SIZE; + + params.fragmentUsseRingBufferMem = fragment_usse_ring_buffer_addr; + params.fragmentUsseRingBufferMemSize = SCE_GXM_DEFAULT_FRAGMENT_USSE_RING_BUFFER_SIZE; + params.fragmentUsseRingBufferOffset = fragment_usse_offset; + + sceGxmCreateContext(¶ms, &gxm_context); +} + +static void AllocRenderTarget(void) { + SceGxmRenderTargetParams params = { 0 }; + + params.width = DISPLAY_WIDTH; + params.height = DISPLAY_HEIGHT; + params.scenesPerFrame = 1; + params.driverMemBlock = -1; + + sceGxmCreateRenderTarget(¶ms, &gxm_render_target); +} + +static void AllocColorBuffer(int i) { + int size = ALIGNUP(4 * DISPLAY_STRIDE * DISPLAY_HEIGHT, 1 * 1024 * 1024); + + gxm_color_surfaces_addr[i] = AllocGPUMemory(size, SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, + SCE_GXM_MEMORY_ATTRIB_RW, &gxm_color_surfaces_uid[i]); + + sceGxmColorSurfaceInit(&gxm_color_surfaces[i], + SCE_GXM_COLOR_FORMAT_A8B8G8R8, + SCE_GXM_COLOR_SURFACE_LINEAR, + SCE_GXM_COLOR_SURFACE_SCALE_NONE, + SCE_GXM_OUTPUT_REGISTER_SIZE_32BIT, + DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_STRIDE, + gxm_color_surfaces_addr[i]); + + sceGxmSyncObjectCreate(&gxm_sync_objects[i]); +} + +static void AllocDepthBuffer(void) { + int width = ALIGNUP(DISPLAY_WIDTH, SCE_GXM_TILE_SIZEX); + int height = ALIGNUP(DISPLAY_HEIGHT, SCE_GXM_TILE_SIZEY); + int samples = width * height; + + gxm_depth_stencil_surface_addr = AllocGPUMemory(4 * samples, SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, + SCE_GXM_MEMORY_ATTRIB_RW, &gxm_depth_stencil_surface_uid); + + sceGxmDepthStencilSurfaceInit(&gxm_depth_stencil_surface, + SCE_GXM_DEPTH_STENCIL_FORMAT_S8D24, + SCE_GXM_DEPTH_STENCIL_SURFACE_TILED, + width, gxm_depth_stencil_surface_addr, NULL); +} + +static void AllocShaderPatcherMemory(void) { + gxm_shader_patcher_buffer_addr = AllocGPUMemory(shader_patcher_buffer_size, + SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, SCE_GXM_MEMORY_ATTRIB_READ, + &gxm_shader_patcher_buffer_uid); + + gxm_shader_patcher_vertex_usse_addr = AllocGPUVertexUSSE( + shader_patcher_vertex_usse_size, &gxm_shader_patcher_vertex_usse_uid, + &shader_patcher_vertex_usse_offset); + + gxm_shader_patcher_fragment_usse_addr = AllocGPUFragmentUSSE( + shader_patcher_fragment_usse_size, &gxm_shader_patcher_fragment_usse_uid, + &shader_patcher_fragment_usse_offset); +} + +static void AllocShaderPatcher(void) { + SceGxmShaderPatcherParams params = { 0 }; + params.hostAllocCallback = AllocShaderPatcherMem; + params.hostFreeCallback = FreeShaderPatcherMem; + + params.bufferMem = gxm_shader_patcher_buffer_addr; + params.bufferMemSize = shader_patcher_buffer_size; + + params.vertexUsseMem = gxm_shader_patcher_vertex_usse_addr; + params.vertexUsseMemSize = shader_patcher_vertex_usse_size; + params.vertexUsseOffset = shader_patcher_vertex_usse_offset; + + params.fragmentUsseMem = gxm_shader_patcher_fragment_usse_addr; + params.fragmentUsseMemSize = shader_patcher_fragment_usse_size; + params.fragmentUsseOffset = shader_patcher_fragment_usse_offset; + + sceGxmShaderPatcherCreate(¶ms, &gxm_shader_patcher); +} + +static void AllocColouredVertexProgram(int index) { + SceGxmShaderPatcherId programID; + VertexProgram* VP = &VP_list[index]; + sceGxmShaderPatcherRegisterProgram(gxm_shader_patcher, gxm_colored_VP, &programID); + + const SceGxmProgram* prog = sceGxmShaderPatcherGetProgramFromId(programID); + + VP->param_in_position = sceGxmProgramFindParameterByName(prog, "in_position"); + VP->param_in_color = sceGxmProgramFindParameterByName(prog, "in_color"); + VP->param_uni_mvp = sceGxmProgramFindParameterByName(prog, "mvp_matrix"); + + SceGxmVertexAttribute attribs[2]; + SceGxmVertexStream vertex_stream; + + attribs[0].streamIndex = 0; + attribs[0].offset = 0; + attribs[0].format = SCE_GXM_ATTRIBUTE_FORMAT_F32; + attribs[0].componentCount = 3; + attribs[0].regIndex = sceGxmProgramParameterGetResourceIndex(VP->param_in_position); + + attribs[1].streamIndex = 0; + attribs[1].offset = 3 * sizeof(float); + attribs[1].format = SCE_GXM_ATTRIBUTE_FORMAT_U8N; + attribs[1].componentCount = 4; + attribs[1].regIndex = sceGxmProgramParameterGetResourceIndex(VP->param_in_color); + + vertex_stream.stride = SIZEOF_VERTEX_COLOURED; + vertex_stream.indexSource = SCE_GXM_INDEX_SOURCE_INDEX_16BIT; + + sceGxmShaderPatcherCreateVertexProgram(gxm_shader_patcher, + programID, attribs, 2, + &vertex_stream, 1, &VP->programPatched); +} + +static void AllocTexturedVertexProgram(int index) { + SceGxmShaderPatcherId programID; + VertexProgram* VP = &VP_list[index]; + sceGxmShaderPatcherRegisterProgram(gxm_shader_patcher, gxm_textured_VP, &programID); + + const SceGxmProgram* prog = sceGxmShaderPatcherGetProgramFromId(programID); + + VP->param_in_position = sceGxmProgramFindParameterByName(prog, "in_position"); + VP->param_in_color = sceGxmProgramFindParameterByName(prog, "in_color"); + VP->param_in_texcoord = sceGxmProgramFindParameterByName(prog, "in_texcoord"); + VP->param_uni_mvp = sceGxmProgramFindParameterByName(prog, "mvp_matrix"); + + SceGxmVertexAttribute attribs[3]; + SceGxmVertexStream vertex_stream; + + attribs[0].streamIndex = 0; + attribs[0].offset = 0; + attribs[0].format = SCE_GXM_ATTRIBUTE_FORMAT_F32; + attribs[0].componentCount = 3; + attribs[0].regIndex = sceGxmProgramParameterGetResourceIndex(VP->param_in_position); + + attribs[1].streamIndex = 0; + attribs[1].offset = 3 * sizeof(float); + attribs[1].format = SCE_GXM_ATTRIBUTE_FORMAT_U8N; + attribs[1].componentCount = 4; + attribs[1].regIndex = sceGxmProgramParameterGetResourceIndex(VP->param_in_color); + + attribs[2].streamIndex = 0; + attribs[2].offset = 3 * sizeof(float) + 4 * sizeof(char); + attribs[2].format = SCE_GXM_ATTRIBUTE_FORMAT_F32; + attribs[2].componentCount = 2; + attribs[2].regIndex = sceGxmProgramParameterGetResourceIndex(VP->param_in_texcoord); + + vertex_stream.stride = SIZEOF_VERTEX_TEXTURED; + vertex_stream.indexSource = SCE_GXM_INDEX_SOURCE_INDEX_16BIT; + + sceGxmShaderPatcherCreateVertexProgram(gxm_shader_patcher, + programID, attribs, 3, + &vertex_stream, 1, &VP->programPatched); +} + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID white_square; +static void SetDefaultStates(void) { + sceGxmSetFrontDepthFunc(gxm_context, SCE_GXM_DEPTH_FUNC_LESS_EQUAL); + sceGxmSetBackDepthFunc(gxm_context, SCE_GXM_DEPTH_FUNC_LESS_EQUAL); +} + +void Gfx_AllocFramebuffers(void) { // called from Window_Init + for (int i = 0; i < NUM_DISPLAY_BUFFERS; i++) + { + AllocColorBuffer(i); + } + AllocDepthBuffer(); + + frontBufferIndex = NUM_DISPLAY_BUFFERS - 1; + backBufferIndex = 0; +} + +static void InitGPU(void) { + AllocRingBuffers(); + AllocGXMContext(); + + AllocRenderTarget(); + AllocShaderPatcherMemory(); + AllocShaderPatcher(); + + AllocColouredVertexProgram(0); + CreateFragmentPrograms(0, gxm_colored_FP, gxm_colored_VP); + CreateFragmentPrograms(6, gxm_colored_alpha_FP, gxm_colored_VP); + AllocTexturedVertexProgram(1); + CreateFragmentPrograms(3, gxm_textured_FP, gxm_textured_VP); + CreateFragmentPrograms(9, gxm_textured_alpha_FP, gxm_textured_VP); +} + +void Gfx_Create(void) { + DQ_OnNextFrame = DQ_OnNextFrame3D; + if (!Gfx.Created) InitGPU(); + in_scene = false; + + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.MaxTexSize = 512 * 512; + Gfx.Created = true; + gfx_vsync = true; + + Gfx_SetDepthTest(true); + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + Gfx_RestoreState(); +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 1x1 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); + // TODO +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + + +/*########################################################################################################################* +*--------------------------------------------------------GPU Textures-----------------------------------------------------* +*#########################################################################################################################*/ +struct GPUTexture; +struct GPUTexture { + cc_uint32* data; + SceUID uid; + SceGxmTexture texture; + struct GPUTexture* next; + cc_uint32 lastFrame; +}; +static struct GPUTexture* del_textures_head; +static struct GPUTexture* del_textures_tail; + +struct GPUTexture* GPUTexture_Alloc(int size) { + struct GPUTexture* tex = Mem_AllocCleared(1, sizeof(struct GPUTexture), "GPU texture"); + + tex->data = AllocGPUMemory(size, + SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, SCE_GXM_MEMORY_ATTRIB_READ, + &tex->uid); + return tex; +} + +// can't delete textures until not used in any frames +static void GPUTexture_Unref(GfxResourceID* resource) { + struct GPUTexture* tex = (struct GPUTexture*)(*resource); + if (!tex) return; + *resource = NULL; + + LinkedList_Append(tex, del_textures_head, del_textures_tail); +} + +static void GPUTexture_Free(struct GPUTexture* tex) { + FreeGPUMemory(tex->uid); + Mem_Free(tex); +} + +static void GPUTextures_DeleteUnreferenced(void) { + if (!del_textures_head) return; + + struct GPUTexture* tex; + struct GPUTexture* next; + struct GPUTexture* prev = NULL; + + for (tex = del_textures_head; tex != NULL; tex = next) + { + next = tex->next; + + if (tex->lastFrame + 4 > frameCounter) { + // texture was used within last 4 fames + prev = tex; + } else { + // advance the head of the linked list + if (del_textures_head == tex) + del_textures_head = next; + + // update end of linked list if necessary + if (del_textures_tail == tex) + del_textures_tail = prev; + + // unlink this texture from the linked list + if (prev) prev->next = next; + + GPUTexture_Free(tex); + } + } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + int size = bmp->width * bmp->height * 4; + struct GPUTexture* tex = GPUTexture_Alloc(size); + CopyTextureData(tex->data, bmp->width * 4, bmp, rowWidth << 2); + + sceGxmTextureInitLinear(&tex->texture, tex->data, + SCE_GXM_TEXTURE_FORMAT_A8B8G8R8, bmp->width, bmp->height, 0); + + sceGxmTextureSetUAddrMode(&tex->texture, SCE_GXM_TEXTURE_ADDR_REPEAT); + sceGxmTextureSetVAddrMode(&tex->texture, SCE_GXM_TEXTURE_ADDR_REPEAT); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + struct GPUTexture* tex = (struct GPUTexture*)texId; + int texWidth = sceGxmTextureGetWidth(&tex->texture); + + // NOTE: Only valid for LINEAR textures + cc_uint32* dst = (tex->data + x) + y * texWidth; + + CopyTextureData(dst, texWidth * 4, part, rowWidth << 2); + // TODO: Do line by line and only invalidate the actually changed parts of lines? + //sceKernelDcacheWritebackInvalidateRange(dst, (tex->width * part->height) * 4); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GPUTexture_Unref(texId); +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + +void Gfx_BindTexture(GfxResourceID texId) { + if (!texId) texId = white_square; + + struct GPUTexture* tex = (struct GPUTexture*)texId; + tex->lastFrame = frameCounter; + sceGxmSetFragmentTexture(gxm_context, 0, &tex->texture); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glortho + // The simplified calculation below uses: L = 0, R = width, T = 0, B = height + // NOTE: Shared with OpenGL. might be wrong to do that though? + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = -2.0f / (zFar - zNear); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = -(zFar + zNear) / (zFar - zNear); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + + // Transposed, source https://learn.microsoft.com/en-us/windows/win32/opengl/glfrustum + // For a FOV based perspective matrix, left/right/top/bottom are calculated as: + // left = -c * aspect, right = c * aspect, bottom = -c, top = c + // Calculations are simplified because of left/right and top/bottom symmetry + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using PS Vita --\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { + in_scene = true; + sceGxmBeginScene(gxm_context, + 0, gxm_render_target, + NULL, NULL, + gxm_sync_objects[backBufferIndex], + &gxm_color_surfaces[backBufferIndex], + &gxm_depth_stencil_surface); +} + +void Gfx_UpdateCommonDialogBuffers(void) { + SceCommonDialogUpdateParam param = { 0 }; + param.renderTarget.colorFormat = SCE_GXM_COLOR_FORMAT_A8B8G8R8; + param.renderTarget.surfaceType = SCE_GXM_COLOR_SURFACE_LINEAR; + param.renderTarget.width = DISPLAY_WIDTH; + param.renderTarget.height = DISPLAY_HEIGHT; + param.renderTarget.strideInPixels = DISPLAY_STRIDE; + param.renderTarget.colorSurfaceData = gxm_color_surfaces_addr[backBufferIndex]; + param.renderTarget.depthSurfaceData = gxm_depth_stencil_surface.depthData; + param.displaySyncObject = gxm_sync_objects[backBufferIndex]; + + sceCommonDialogUpdate(¶m); +} + +void Gfx_NextFramebuffer(void) { + struct DQCallbackData cb_data; + cb_data.addr = gxm_color_surfaces_addr[backBufferIndex]; + + sceGxmDisplayQueueAddEntry(gxm_sync_objects[frontBufferIndex], + gxm_sync_objects[backBufferIndex], &cb_data); + + // Cycle through to next buffer pair + frontBufferIndex = backBufferIndex; + backBufferIndex = (backBufferIndex + 1) % NUM_DISPLAY_BUFFERS; +} + +void Gfx_EndFrame(void) { + in_scene = false; + sceGxmEndScene(gxm_context, NULL, NULL); + + Gfx_NextFramebuffer(); + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_OnWindowResize(void) { } + +void Gfx_SetViewport(int x, int y, int w, int h) { } + + +/*########################################################################################################################* +*--------------------------------------------------------GPU Buffers------------------------------------------------------* +*#########################################################################################################################*/ +struct GPUBuffer; +struct GPUBuffer { + void* data; + SceUID uid; + cc_uint32 lastFrame; + struct GPUBuffer* next; +}; +static struct GPUBuffer* del_buffers_head; +static struct GPUBuffer* del_buffers_tail; + +struct GPUBuffer* GPUBuffer_Alloc(int size) { + struct GPUBuffer* buffer = Mem_AllocCleared(1, sizeof(struct GPUBuffer), "GPU buffer"); + + buffer->data = AllocGPUMemory(size, + SCE_KERNEL_MEMBLOCK_TYPE_USER_RW_UNCACHE, SCE_GXM_MEMORY_ATTRIB_READ, + &buffer->uid); + return buffer; +} + + +// can't delete buffers until not used in any frames +static void GPUBuffer_Unref(GfxResourceID* resource) { + struct GPUBuffer* buf = (struct GPUBuffer*)(*resource); + if (!buf) return; + *resource = NULL; + + LinkedList_Append(buf, del_buffers_head, del_buffers_tail); +} + +static void GPUBuffer_Free(struct GPUBuffer* buf) { + FreeGPUMemory(buf->uid); + Mem_Free(buf); +} + +static void GPUBuffers_DeleteUnreferenced(void) { + if (!del_buffers_head) return; + + struct GPUBuffer* buf; + struct GPUBuffer* next; + struct GPUBuffer* prev = NULL; + + for (buf = del_buffers_head; buf != NULL; buf = next) + { + next = buf->next; + + if (buf->lastFrame + 4 > frameCounter) { + // texture was used within last 4 fames + prev = buf; + } else { + // advance the head of the linked list + if (del_buffers_head == buf) + del_buffers_head = next; + + // update end of linked list if necessary + if (del_buffers_tail == buf) + del_buffers_tail = prev; + + // unlink this texture from the linked list + if (prev) prev->next = next; + + GPUBuffer_Free(buf); + } + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint16* gfx_indices; + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + struct GPUBuffer* buffer = GPUBuffer_Alloc(count * 2); + fillFunc(buffer->data, count, obj); + return buffer; +} + +void Gfx_BindIb(GfxResourceID ib) { + struct GPUBuffer* buffer = (struct GPUBuffer*)ib; + gfx_indices = buffer->data; +} + +void Gfx_DeleteIb(GfxResourceID* ib) { GPUBuffer_Unref(ib); } + + +/*########################################################################################################################* +*-------------------------------------------------------Vertex buffers----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return GPUBuffer_Alloc(count * strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + buffer->lastFrame = frameCounter; + sceGxmSetVertexStream(gxm_context, 0, buffer->data); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { GPUBuffer_Unref(vb); } + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + return buffer->data; +} + +void Gfx_UnlockVb(GfxResourceID vb) { Gfx_BindVb(vb); } + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return GPUBuffer_Alloc(maxVertices * strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + struct GPUBuffer* buffer = (struct GPUBuffer*)vb; + return buffer->data; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) { + // TODO +} + +void Gfx_SetFogCol(PackedCol color) { + // TODO +} + +void Gfx_SetFogDensity(float value) { + // TODO +} + +void Gfx_SetFogEnd(float value) { + // TODO +} + +void Gfx_SetFogMode(FogFunc func) { + // TODO +} + +static void SetAlphaTest(cc_bool enabled) { + FP_SwitchActive(); +} + +static void SetAlphaBlend(cc_bool enabled) { + FP_SwitchActive(); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + // TODO + gfx_depthOnly = depthOnly; + FP_SwitchActive(); +} + +void Gfx_SetFaceCulling(cc_bool enabled) { + sceGxmSetCullMode(gxm_context, enabled ? SCE_GXM_CULL_CW : SCE_GXM_CULL_NONE); +} + + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static PackedCol clear_color; +void Gfx_ClearColor(PackedCol color) { + clear_color = color; +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + int mode = enabled ? SCE_GXM_DEPTH_WRITE_ENABLED : SCE_GXM_DEPTH_WRITE_DISABLED; + sceGxmSetFrontDepthWriteEnable(gxm_context, mode); + sceGxmSetBackDepthWriteEnable(gxm_context, mode); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + int func = enabled ? SCE_GXM_DEPTH_FUNC_LESS_EQUAL : SCE_GXM_DEPTH_FUNC_ALWAYS; + sceGxmSetFrontDepthFunc(gxm_context, func); + sceGxmSetBackDepthFunc(gxm_context, func); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + struct Matrix mvp __attribute__((aligned(64))); + Matrix_Mul(&mvp, &_view, &_proj); + float* m = &mvp; + + // Transpose matrix + for (int i = 0; i < 4; i++) + { + transposed_mvp[i * 4 + 0] = m[0 + i]; + transposed_mvp[i * 4 + 1] = m[4 + i]; + transposed_mvp[i * 4 + 2] = m[8 + i]; + transposed_mvp[i * 4 + 3] = m[12 + i]; + } + + VP_DirtyUniform(VP_UNI_MATRIX); + VP_ReloadUniforms(); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + // TODO +} + +void Gfx_DisableTextureOffset(void) { + // TODO +} + + + +/*########################################################################################################################* +*---------------------------------------------------------Drawing---------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + VP_SwitchActive(); + FP_SwitchActive(); +} + +void Gfx_DrawVb_Lines(int verticesCount) { + // TODO +} + +// TODO probably wrong to offset index buffer +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + //Platform_Log2("DRAW1: %i, %i", &verticesCount, &startVertex); Thread_Sleep(100); + sceGxmDraw(gxm_context, SCE_GXM_PRIMITIVE_TRIANGLES, + SCE_GXM_INDEX_FORMAT_U16, gfx_indices + ICOUNT(startVertex), ICOUNT(verticesCount)); +} + +// TODO probably wrong to offset index buffer +void Gfx_DrawVb_IndexedTris(int verticesCount) { + //Platform_Log1("DRAW2: %i", &verticesCount); Thread_Sleep(100); + sceGxmDraw(gxm_context, SCE_GXM_PRIMITIVE_TRIANGLES, + SCE_GXM_INDEX_FORMAT_U16, gfx_indices, ICOUNT(verticesCount)); +} + +// TODO probably wrong to offset index buffer +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + //Platform_Log2("DRAW3: %i, %i", &verticesCount, &startVertex); Thread_Sleep(100); + sceGxmDraw(gxm_context, SCE_GXM_PRIMITIVE_TRIANGLES, + SCE_GXM_INDEX_FORMAT_U16, gfx_indices + ICOUNT(startVertex), ICOUNT(verticesCount)); +} + + +void Gfx_ClearBuffers(GfxBuffers buffers) { + // TODO clear only some buffers + static struct GPUBuffer* clearVB; + if (!clearVB) { + clearVB = GPUBuffer_Alloc(4 * sizeof(struct VertexColoured)); + } + + struct VertexColoured* clear_vertices = clearVB->data; + clear_vertices[0] = (struct VertexColoured){-1.0f, -1.0f, 1.0f, clear_color }; + clear_vertices[1] = (struct VertexColoured){ 1.0f, -1.0f, 1.0f, clear_color }; + clear_vertices[2] = (struct VertexColoured){ 1.0f, 1.0f, 1.0f, clear_color }; + clear_vertices[3] = (struct VertexColoured){-1.0f, 1.0f, 1.0f, clear_color }; + + Gfx_SetAlphaTest(false); + Gfx_SetDepthTest(false); + + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + Gfx_LoadIdentityMatrix(MATRIX_VIEW); + Gfx_LoadIdentityMatrix(MATRIX_PROJECTION); + Gfx_BindVb(clearVB); + Gfx_DrawVb_IndexedTris(4); + + Gfx_SetDepthTest(true); +} +#endif diff --git a/src/Graphics_Saturn.c b/src/Graphics_Saturn.c new file mode 100644 index 0000000..e91937d --- /dev/null +++ b/src/Graphics_Saturn.c @@ -0,0 +1,565 @@ +#include "Core.h" +#if defined CC_BUILD_SATURN +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#include +#include + + +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 224 + +#define PRIMITIVE_DRAW_MODE_NORMAL (0) +#define PRIMITIVE_DRAW_MODE_MESH (1) +#define PRIMITIVE_DRAW_MODE_SHADOW (2) +#define PRIMITIVE_DRAW_MODE_HALF_LUMINANCE (3) +#define PRIMITIVE_DRAW_MODE_HALF_TRANSPARENT (4) +#define PRIMITIVE_DRAW_MODE_GOURAUD_SHADING (5) +#define PRIMITIVE_DRAW_MODE_GOURAUD_HALF_LUM (6) +#define PRIMITIVE_DRAW_MODE_GOURAUD_HALF_TRANS (7) +#define PRIMITIVE_DRAW_MODE_COUNT (8) + +#define CMDS_COUNT 400 + +static PackedCol clear_color; +static vdp1_cmdt_t cmdts_all[CMDS_COUNT]; +static int cmdts_count; +static vdp1_vram_partitions_t _vdp1_vram_partitions; + +static vdp1_cmdt_t* NextPrimitive(void) { + if (cmdts_count >= CMDS_COUNT) Logger_Abort("Too many VDP1 commands"); + return &cmdts_all[cmdts_count++]; +} + +static const vdp1_cmdt_draw_mode_t color_draw_mode = { + .raw = 0x0000 +}; +static const vdp1_cmdt_draw_mode_t texture_draw_mode = { + .cc_mode = PRIMITIVE_DRAW_MODE_GOURAUD_SHADING +}; + +static void UpdateVDP1Env(void) { + vdp1_env_t env; + vdp1_env_default_init(&env); + + int R = PackedCol_R(clear_color); + int G = PackedCol_G(clear_color); + int B = PackedCol_B(clear_color); + env.erase_color = RGB1555(1, R >> 3, G >> 3, B >> 3); + + vdp1_env_set(&env); +} + +static void CalcGouraudColours(void) { + for (int i = 0; i < 1024; i++) + { + // 1024 = 10 bits, divided into RRR GGGG BBB + int r_idx = (i & 0x007) >> 0, R = r_idx << (5 - 3); + int g_idx = (i & 0x078) >> 3, G = g_idx << (5 - 4); + int b_idx = (i & 0x380) >> 7, B = b_idx << (5 - 3); + rgb1555_t gouraud = RGB1555(1, R, G, B); + + vdp1_gouraud_table_t* cur = &_vdp1_vram_partitions.gouraud_base[i]; + cur->colors[0] = gouraud; + cur->colors[1] = gouraud; + cur->colors[2] = gouraud; + cur->colors[3] = gouraud; + } +} + +static GfxResourceID white_square; +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 2x2 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[4] = { BitmapColor_RGB(255, 0, 0), BITMAPCOLOR_WHITE, BITMAPCOLOR_WHITE, BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 2, 2, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +void Gfx_Create(void) { + if (!Gfx.Created) { + vdp1_vram_partitions_get(&_vdp1_vram_partitions); + // TODO less ram for gourad base + vdp2_scrn_back_color_set(VDP2_VRAM_ADDR(3, 0x01FFFE), + RGB1555(1, 0, 3, 15)); + vdp2_sprite_priority_set(0, 6); + + UpdateVDP1Env(); + CalcGouraudColours(); + } + + Gfx.MinTexWidth = 8; + Gfx.MinTexHeight = 8; + Gfx.MaxTexWidth = 128; + Gfx.MaxTexHeight = 128; + Gfx.Created = true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +#define BGRA8_to_SATURN(src) \ + ((src[2] & 0xF8) >> 3) | ((src[1] & 0xF8) << 2) | ((src[0] & 0xF8) << 7) | ((src[3] & 0x80) << 8) + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + cc_uint16* tmp = Mem_TryAlloc(bmp->width * bmp->height, 2); + if (!tmp) return NULL; + + for (int y = 0; y < bmp->height; y++) + { + cc_uint32* src = bmp->scan0 + y * rowWidth; + cc_uint16* dst = tmp + y * bmp->width; + + for (int x = 0; x < bmp->width; x++) + { + cc_uint8* color = (cc_uint8*)&src[x]; + dst[x] = BGRA8_to_SATURN(color); + dst[x] = 0xFEEE; + } + } + + scu_dma_transfer(0, _vdp1_vram_partitions.texture_base, tmp, bmp->width * bmp->height * 2); + scu_dma_transfer_wait(0); + Mem_Free(tmp); + return (void*)1; +} + +void Gfx_BindTexture(GfxResourceID texId) { +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + // TODO +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*------------------------------------------------------State management---------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) { } +void Gfx_SetFogCol(PackedCol col) { } +void Gfx_SetFogDensity(float value) { } +void Gfx_SetFogEnd(float value) { } +void Gfx_SetFogMode(FogFunc func) { } + +void Gfx_SetFaceCulling(cc_bool enabled) { + // TODO +} + +static void SetAlphaTest(cc_bool enabled) { +} + +static void SetAlphaBlend(cc_bool enabled) { +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +void Gfx_ClearBuffers(GfxBuffers buffers) { +} + +void Gfx_ClearColor(PackedCol color) { + if (color == clear_color) return; + + clear_color = color; + UpdateVDP1Env(); +} + +void Gfx_SetDepthTest(cc_bool enabled) { +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + // TODO +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*-------------------------------------------------------Vertex buffers----------------------------------------------------* +*#########################################################################################################################*/ +static void* gfx_vertices; + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return Mem_TryAlloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj, mvp; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + Matrix_Mul(&mvp, &_view, &_proj); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + // TODO +} + +void Gfx_DisableTextureOffset(void) { + // TODO +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.01f; + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh */ + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zNear - zFar); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Rendering-------------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; +} + +void Gfx_DrawVb_Lines(int verticesCount) { + +} + +static void Transform(Vec3* result, struct VertexTextured* a, const struct Matrix* mat) { + /* a could be pointing to result - therefore can't directly assign X/Y/Z */ + float x = a->x * mat->row1.x + a->y * mat->row2.x + a->z * mat->row3.x + mat->row4.x; + float y = a->x * mat->row1.y + a->y * mat->row2.y + a->z * mat->row3.y + mat->row4.y; + float z = a->x * mat->row1.z + a->y * mat->row2.z + a->z * mat->row3.z + mat->row4.z; + float w = a->x * mat->row1.w + a->y * mat->row2.w + a->z * mat->row3.w + mat->row4.w; + + result->x = (x/w) * (SCREEN_WIDTH / 2); + result->y = (y/w) * -(SCREEN_HEIGHT / 2); + result->z = (z/w) * 1024; +} + +#define IsPointCulled(vec) vec.x < -2048 || vec.x > 2048 || vec.y < -2048 || vec.y > 2048 || vec.z < 0 || vec.z > 1024 + +static void DrawColouredQuads2D(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexColoured* v = (struct VertexColoured*)gfx_vertices + startVertex + i; + + int16_vec2_t points[4]; + points[0].x = (int)v[0].x - SCREEN_WIDTH / 2; points[0].y = (int)v[0].y - SCREEN_HEIGHT / 2; + points[1].x = (int)v[1].x - SCREEN_WIDTH / 2; points[1].y = (int)v[1].y - SCREEN_HEIGHT / 2; + points[2].x = (int)v[2].x - SCREEN_WIDTH / 2; points[2].y = (int)v[2].y - SCREEN_HEIGHT / 2; + points[3].x = (int)v[3].x - SCREEN_WIDTH / 2; points[3].y = (int)v[3].y - SCREEN_HEIGHT / 2; + + int R = PackedCol_R(v->Col); + int G = PackedCol_G(v->Col); + int B = PackedCol_B(v->Col); + + vdp1_cmdt_t* cmd; + + cmd = NextPrimitive(); + vdp1_cmdt_polygon_set(cmd); + vdp1_cmdt_color_set(cmd, RGB1555(1, R >> 3, G >> 3, B >> 3)); + vdp1_cmdt_draw_mode_set(cmd, color_draw_mode); + vdp1_cmdt_vtx_set(cmd, points); + } +} + +static void DrawTexturedQuads2D(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + int16_vec2_t points[4]; + points[0].x = (int)v[0].x - SCREEN_WIDTH / 2; points[0].y = (int)v[0].y - SCREEN_HEIGHT / 2; + points[1].x = (int)v[1].x - SCREEN_WIDTH / 2; points[1].y = (int)v[1].y - SCREEN_HEIGHT / 2; + points[2].x = (int)v[2].x - SCREEN_WIDTH / 2; points[2].y = (int)v[2].y - SCREEN_HEIGHT / 2; + points[3].x = (int)v[3].x - SCREEN_WIDTH / 2; points[3].y = (int)v[3].y - SCREEN_HEIGHT / 2; + + int R = PackedCol_R(v->Col); + int G = PackedCol_G(v->Col); + int B = PackedCol_B(v->Col); + + vdp1_cmdt_t* cmd; + + cmd = NextPrimitive(); + vdp1_cmdt_polygon_set(cmd); + vdp1_cmdt_color_set(cmd, RGB1555(1, R >> 3, G >> 3, B >> 3)); + vdp1_cmdt_draw_mode_set(cmd, color_draw_mode); + vdp1_cmdt_vtx_set(cmd, points); + + /*cmd = NextPrimitive(); + vdp1_cmdt_distorted_sprite_set(cmd); + vdp1_cmdt_char_size_set(cmd, 8, 8); + vdp1_cmdt_char_base_set(cmd, (vdp1_vram_t)_vdp1_vram_partitions.texture_base); + vdp1_cmdt_draw_mode_set(cmd, texture_draw_mode); + vdp1_cmdt_vtx_set(cmd, points);*/ + } +} + +static void DrawColouredQuads3D(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexColoured* v = (struct VertexColoured*)gfx_vertices + startVertex + i; + + Vec3 coords[4]; + Transform(&coords[0], &v[0], &mvp); + Transform(&coords[1], &v[1], &mvp); + Transform(&coords[2], &v[2], &mvp); + Transform(&coords[3], &v[3], &mvp); + + int16_vec2_t points[4]; + points[0].x = coords[0].x; points[0].y = coords[0].y; + points[1].x = coords[1].x; points[1].y = coords[1].y; + points[2].x = coords[2].x; points[2].y = coords[2].y; + points[3].x = coords[3].x; points[3].y = coords[3].y; + + if (IsPointCulled(coords[0])) continue; + if (IsPointCulled(coords[1])) continue; + if (IsPointCulled(coords[2])) continue; + if (IsPointCulled(coords[3])) continue; + + int R = PackedCol_R(v->Col); + int G = PackedCol_G(v->Col); + int B = PackedCol_B(v->Col); + + vdp1_cmdt_t* cmd; + + cmd = NextPrimitive(); + vdp1_cmdt_polygon_set(cmd); + vdp1_cmdt_color_set(cmd, RGB1555(1, R >> 3, G >> 3, B >> 3)); + vdp1_cmdt_draw_mode_set(cmd, color_draw_mode); + vdp1_cmdt_vtx_set(cmd, points); + } +} + +static void DrawTexturedQuads3D(int verticesCount, int startVertex) { + for (int i = 0; i < verticesCount; i += 4) + { + struct VertexTextured* v = (struct VertexTextured*)gfx_vertices + startVertex + i; + + Vec3 coords[4]; + Transform(&coords[0], &v[0], &mvp); + Transform(&coords[1], &v[1], &mvp); + Transform(&coords[2], &v[2], &mvp); + Transform(&coords[3], &v[3], &mvp); + + int16_vec2_t points[4]; + points[0].x = coords[0].x; points[0].y = coords[0].y; + points[1].x = coords[1].x; points[1].y = coords[1].y; + points[2].x = coords[2].x; points[2].y = coords[2].y; + points[3].x = coords[3].x; points[3].y = coords[3].y; + + if (IsPointCulled(coords[0])) continue; + if (IsPointCulled(coords[1])) continue; + if (IsPointCulled(coords[2])) continue; + if (IsPointCulled(coords[3])) continue; + + int R = PackedCol_R(v->Col); + int G = PackedCol_G(v->Col); + int B = PackedCol_B(v->Col); + + vdp1_cmdt_t* cmd; + + cmd = NextPrimitive(); + vdp1_cmdt_polygon_set(cmd); + vdp1_cmdt_color_set(cmd, RGB1555(1, R >> 3, G >> 3, B >> 3)); + vdp1_cmdt_draw_mode_set(cmd, color_draw_mode); + vdp1_cmdt_vtx_set(cmd, points); + + /*cmd = NextPrimitive(); + vdp1_cmdt_distorted_sprite_set(cmd); + vdp1_cmdt_char_size_set(cmd, 8, 8); + vdp1_cmdt_char_base_set(cmd, (vdp1_vram_t)_vdp1_vram_partitions.texture_base); + vdp1_cmdt_draw_mode_set(cmd, texture_draw_mode); + vdp1_cmdt_vtx_set(cmd, points);*/ + } +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + if (gfx_rendering2D) { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + DrawTexturedQuads2D(verticesCount, startVertex); + } else { + DrawColouredQuads2D(verticesCount, startVertex); + } + } else { + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + DrawTexturedQuads3D(verticesCount, startVertex); + } else { + DrawColouredQuads3D(verticesCount, startVertex); + } + } +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + Gfx_DrawVb_IndexedTris_Range(verticesCount, 0); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + DrawTexturedQuads3D(verticesCount, startVertex); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Other/Misc------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +cc_bool Gfx_WarnIfNecessary(void) { + return false; +} + +void Gfx_BeginFrame(void) { + Platform_LogConst("FRAME BEG"); + cmdts_count = 0; + + static const int16_vec2_t system_clip_coord = { SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1 }; + static const int16_vec2_t local_coord_center = { SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2 }; + + vdp1_cmdt_t* cmd; + + cmd = NextPrimitive(); + vdp1_cmdt_system_clip_coord_set(cmd); + vdp1_cmdt_vtx_system_clip_coord_set(cmd, system_clip_coord); + + cmd = NextPrimitive(); + vdp1_cmdt_local_coord_set(cmd); + vdp1_cmdt_vtx_local_coord_set(cmd, local_coord_center); +} + +void Gfx_EndFrame(void) { + Platform_LogConst("FRAME END"); + vdp1_cmdt_t* cmd; + + cmd = NextPrimitive(); + vdp1_cmdt_end_set(cmd); + + vdp1_cmdt_list_t cmdt_list; + cmdt_list.cmdts = cmdts_all; + cmdt_list.count = cmdts_count; + vdp1_sync_cmdt_list_put(&cmdt_list, 0); + + vdp1_sync_render(); + vdp1_sync(); + vdp2_sync(); + vdp2_sync_wait(); + + if (gfx_minFrameMs) LimitFPS(); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_OnWindowResize(void) { + // TODO +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using Saturn --\n"); + PrintMaxTextureInfo(info); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } +#endif diff --git a/src/Graphics_SoftGPU.c b/src/Graphics_SoftGPU.c new file mode 100644 index 0000000..275912c --- /dev/null +++ b/src/Graphics_SoftGPU.c @@ -0,0 +1,674 @@ +#include "Core.h" +#if CC_GFX_BACKEND == CC_GFX_BACKEND_SOFTGPU +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" + +static cc_bool faceCulling; +static int fb_width, fb_height; +static struct Bitmap fb_bmp; +static float vp_hwidth, vp_hheight; +static int fb_maxX, fb_maxY; + +static BitmapCol* colorBuffer; +static BitmapCol clearColor; +static cc_bool colWrite = true; +static int cb_stride; + +static float* depthBuffer; +static cc_bool depthTest = true; +static cc_bool depthWrite = true; +static int db_stride; + +static void* gfx_vertices; +static GfxResourceID white_square; + +void Gfx_RestoreState(void) { + InitDefaultResources(); + + // 1x1 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +void Gfx_Create(void) { + Gfx.MaxTexWidth = 4096; + Gfx.MaxTexHeight = 4096; + Gfx.Created = true; + + Gfx_RestoreState(); +} + +static void DestroyBuffers(void) { + Window_FreeFramebuffer(&fb_bmp); + Mem_Free(depthBuffer); + depthBuffer = NULL; +} + +void Gfx_Free(void) { + Gfx_FreeState(); + DestroyBuffers(); +} + + +typedef struct CCTexture { + int width, height; + BitmapCol pixels[]; +} CCTexture; + +static CCTexture* curTexture; +static BitmapCol* curTexPixels; +static int curTexWidth, curTexHeight; +static int texWidthMask, texHeightMask; + +void Gfx_BindTexture(GfxResourceID texId) { + if (!texId) texId = white_square; + CCTexture* tex = texId; + + curTexture = tex; + curTexPixels = tex->pixels; + curTexWidth = tex->width; + curTexHeight = tex->height; + + texWidthMask = (1 << Math_ilog2(tex->width)) - 1; + texHeightMask = (1 << Math_ilog2(tex->height)) - 1; +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + GfxResourceID data = *texId; + if (data) Mem_Free(data); + *texId = NULL; +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)Mem_Alloc(2 + bmp->width * bmp->height, 4, "Texture"); + + tex->width = bmp->width; + tex->height = bmp->height; + CopyTextureData(tex->pixels, bmp->width * 4, bmp, rowWidth << 2); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)texId; + cc_uint32* dst = (tex->pixels + x) + y * tex->width; + CopyTextureData(dst, tex->width * 4, part, rowWidth << 2); +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + + +/*########################################################################################################################* +*------------------------------------------------------State management---------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) { } +void Gfx_SetFogCol(PackedCol col) { } +void Gfx_SetFogDensity(float value) { } +void Gfx_SetFogEnd(float value) { } +void Gfx_SetFogMode(FogFunc func) { } + +void Gfx_SetFaceCulling(cc_bool enabled) { + faceCulling = enabled; +} + +static void SetAlphaTest(cc_bool enabled) { + /* Uses value from Gfx_SetAlphaTest */ +} + +static void SetAlphaBlend(cc_bool enabled) { + /* Uses value from Gfx_SetAlphaBlending */ +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static void ClearColorBuffer(void) { + int i, x, y, size = fb_width * fb_height; + + if (cb_stride == fb_width) { + for (i = 0; i < size; i++) colorBuffer[i] = clearColor; + } else { + /* Slower partial buffer clear */ + for (y = 0; y < fb_height; y++) { + i = y * cb_stride; + for (x = 0; x < fb_width; x++) { + colorBuffer[i + x] = clearColor; + } + } + } +} + +static void ClearDepthBuffer(void) { + int i, size = fb_width * fb_height; + for (i = 0; i < size; i++) depthBuffer[i] = 100000000.0f; +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + if (buffers & GFX_BUFFER_COLOR) ClearColorBuffer(); + if (buffers & GFX_BUFFER_DEPTH) ClearDepthBuffer(); +} + +void Gfx_ClearColor(PackedCol color) { + int R = PackedCol_R(color); + int G = PackedCol_G(color); + int B = PackedCol_B(color); + int A = PackedCol_A(color); + + clearColor = BitmapCol_Make(R, G, B, A); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + depthTest = enabled; +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + depthWrite = enabled; +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + colWrite = !depthOnly; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +/*########################################################################################################################* +*-------------------------------------------------------Vertex buffers----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return Mem_TryAlloc(count, strideSizes[fmt]); +} + +void Gfx_BindVb(GfxResourceID vb) { gfx_vertices = vb; } + +void Gfx_DeleteVb(GfxResourceID* vb) { + GfxResourceID data = *vb; + if (data) Mem_Free(data); + *vb = 0; +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockVb(GfxResourceID vb) { + gfx_vertices = vb; +} + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Mem_TryAlloc(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { + gfx_vertices = vb; +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static float texOffsetX, texOffsetY; +static struct Matrix _view, _proj, mvp; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + Matrix_Mul(&mvp, &_view, &_proj); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + texOffsetX = x; + texOffsetY = y; +} + +void Gfx_DisableTextureOffset(void) { + texOffsetX = 0; + texOffsetY = 0; +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zNear - zFar); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Rendering-------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct Vector3 { float x, y, z; } Vector3; +typedef struct Vector2 { float x, y; } Vector2; +typedef struct Vertex_ { + float x, y, z, w; + float u, v; + PackedCol c; +} Vertex; + +static void TransformVertex2D(int index, Vertex* vertex) { + // TODO: avoid the multiply, just add down in DrawTriangles + char* ptr = (char*)gfx_vertices + index * gfx_stride; + Vector3* pos = (Vector3*)ptr; + vertex->x = pos->x; + vertex->y = pos->y; + + if (gfx_format != VERTEX_FORMAT_TEXTURED) { + struct VertexColoured* v = (struct VertexColoured*)ptr; + vertex->u = 0.0f; + vertex->v = 0.0f; + vertex->c = v->Col; + } else { + struct VertexTextured* v = (struct VertexTextured*)ptr; + vertex->u = v->U; + vertex->v = v->V; + vertex->c = v->Col; + } +} + +static void TransformVertex3D(int index, Vertex* vertex) { + // TODO: avoid the multiply, just add down in DrawTriangles + char* ptr = (char*)gfx_vertices + index * gfx_stride; + Vector3* pos = (Vector3*)ptr; + + struct Vec4 coord; + coord.x = pos->x * mvp.row1.x + pos->y * mvp.row2.x + pos->z * mvp.row3.x + mvp.row4.x; + coord.y = pos->x * mvp.row1.y + pos->y * mvp.row2.y + pos->z * mvp.row3.y + mvp.row4.y; + coord.z = pos->x * mvp.row1.z + pos->y * mvp.row2.z + pos->z * mvp.row3.z + mvp.row4.z; + coord.w = pos->x * mvp.row1.w + pos->y * mvp.row2.w + pos->z * mvp.row3.w + mvp.row4.w; + + float invW = 1.0f / coord.w; + vertex->x = vp_hwidth * (1 + coord.x * invW); + vertex->y = vp_hheight * (1 - coord.y * invW); + vertex->z = coord.z * invW; + vertex->w = invW; + + if (gfx_format != VERTEX_FORMAT_TEXTURED) { + struct VertexColoured* v = (struct VertexColoured*)ptr; + vertex->u = 0.0f; + vertex->v = 0.0f; + vertex->c = v->Col; + } else { + struct VertexTextured* v = (struct VertexTextured*)ptr; + vertex->u = (v->U + texOffsetX) * invW; + vertex->v = (v->V + texOffsetY) * invW; + vertex->c = v->Col; + } +} + +// Ensure it's inlined, whereas Math_FloorF might not be +static CC_INLINE int FastFloor(float value) { + int valueI = (int)value; + return valueI > value ? valueI - 1 : valueI; +} + +#define edgeFunction(ax,ay, bx,by, cx,cy) (((bx) - (ax)) * ((cy) - (ay)) - ((by) - (ay)) * ((cx) - (ax))) + +static void DrawTriangle2D(Vertex* V0, Vertex* V1, Vertex* V2) { + int x0 = (int)V0->x, y0 = (int)V0->y; + int x1 = (int)V1->x, y1 = (int)V1->y; + int x2 = (int)V2->x, y2 = (int)V2->y; + int minX = min(x0, min(x1, x2)); + int minY = min(y0, min(y1, y2)); + int maxX = max(x0, max(x1, x2)); + int maxY = max(y0, max(y1, y2)); + + int area = edgeFunction(x0,y0, x1,y1, x2,y2); + // Reject triangles completely outside + if (maxX < 0 || minX > fb_maxX) return; + if (maxY < 0 || minY > fb_maxY) return; + + // Perform scissoring + minX = max(minX, 0); maxX = min(maxX, fb_maxX); + minY = max(minY, 0); maxY = min(maxY, fb_maxY); + float factor = 1.0f / area; + + float u0 = V0->u * curTexWidth, u1 = V1->u * curTexWidth, u2 = V2->u * curTexWidth; + float v0 = V0->v * curTexHeight, v1 = V1->v * curTexHeight, v2 = V2->v * curTexHeight; + PackedCol color = V0->c; + + // https://fgiesen.wordpress.com/2013/02/10/optimizing-the-basic-rasterizer/ + // Essentially these are the deltas of edge functions between X/Y and X/Y + 1 (i.e. one X/Y step) + int dx01 = y0 - y1, dy01 = x1 - x0; + int dx12 = y1 - y2, dy12 = x2 - x1; + int dx20 = y2 - y0, dy20 = x0 - x2; + + float bc0_start = edgeFunction(x1,y1, x2,y2, minX+0.5f,minY+0.5f); + float bc1_start = edgeFunction(x2,y2, x0,y0, minX+0.5f,minY+0.5f); + float bc2_start = edgeFunction(x0,y0, x1,y1, minX+0.5f,minY+0.5f); + + for (int y = minY; y <= maxY; y++, bc0_start += dy12, bc1_start += dy20, bc2_start += dy01) + { + float bc0 = bc0_start; + float bc1 = bc1_start; + float bc2 = bc2_start; + + for (int x = minX; x <= maxX; x++, bc0 += dx12, bc1 += dx20, bc2 += dx01) + { + float ic0 = bc0 * factor; + float ic1 = bc1 * factor; + float ic2 = bc2 * factor; + + if (ic0 < 0 || ic1 < 0 || ic2 < 0) continue; + int cb_index = y * cb_stride + x; + + int R, G, B, A; + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + float u = ic0 * u0 + ic1 * u1 + ic2 * u2; + float v = ic0 * v0 + ic1 * v1 + ic2 * v2; + int texX = ((int)u) & texWidthMask; + int texY = ((int)v) & texHeightMask; + int texIndex = texY * curTexWidth + texX; + + BitmapCol tColor = curTexPixels[texIndex]; + int a1 = PackedCol_A(color), a2 = BitmapCol_A(tColor); + A = ( a1 * a2 ) >> 8; + int r1 = PackedCol_R(color), r2 = BitmapCol_R(tColor); + R = ( r1 * r2 ) >> 8; + int g1 = PackedCol_G(color), g2 = BitmapCol_G(tColor); + G = ( g1 * g2 ) >> 8; + int b1 = PackedCol_B(color), b2 = BitmapCol_B(tColor); + B = ( b1 * b2 ) >> 8; + } else { + R = PackedCol_R(color); + G = PackedCol_G(color); + B = PackedCol_B(color); + A = PackedCol_A(color); + } + + if (gfx_alphaBlend) { + BitmapCol dst = colorBuffer[cb_index]; + int dstR = BitmapCol_R(dst); + int dstG = BitmapCol_G(dst); + int dstB = BitmapCol_B(dst); + + R = (R * A + dstR * (255 - A)) >> 8; + G = (G * A + dstG * (255 - A)) >> 8; + B = (B * A + dstB * (255 - A)) >> 8; + } + if (gfx_alphaTest && A < 0x80) continue; + + colorBuffer[cb_index] = BitmapCol_Make(R, G, B, 0xFF); + } + } +} + +static void DrawTriangle3D(Vertex* V0, Vertex* V1, Vertex* V2) { + int x0 = (int)V0->x, y0 = (int)V0->y; + int x1 = (int)V1->x, y1 = (int)V1->y; + int x2 = (int)V2->x, y2 = (int)V2->y; + int minX = min(x0, min(x1, x2)); + int minY = min(y0, min(y1, y2)); + int maxX = max(x0, max(x1, x2)); + int maxY = max(y0, max(y1, y2)); + + int area = edgeFunction(x0,y0, x1,y1, x2,y2); + if (faceCulling) { + // https://gamedev.stackexchange.com/questions/203694/how-to-make-backface-culling-work-correctly-in-both-orthographic-and-perspective + if (area < 0) return; + } + + // Reject triangles completely outside + if (maxX < 0 || minX > fb_maxX) return; + if (maxY < 0 || minY > fb_maxY) return; + + // Perform scissoring + minX = max(minX, 0); maxX = min(maxX, fb_maxX); + minY = max(minY, 0); maxY = min(maxY, fb_maxY); + + // NOTE: W in frag variables below is actually 1/W + float factor = 1.0f / area; + float w0 = V0->w, w1 = V1->w, w2 = V2->w; + + // TODO proper clipping + if (w0 <= 0 || w1 <= 0 || w2 <= 0) return; + + float z0 = V0->z, z1 = V1->z, z2 = V2->z; + float u0 = V0->u, u1 = V1->u, u2 = V2->u; + float v0 = V0->v, v1 = V1->v, v2 = V2->v; + PackedCol color = V0->c; + + // https://fgiesen.wordpress.com/2013/02/10/optimizing-the-basic-rasterizer/ + // Essentially these are the deltas of edge functions between X/Y and X/Y + 1 (i.e. one X/Y step) + int dx01 = y0 - y1, dy01 = x1 - x0; + int dx12 = y1 - y2, dy12 = x2 - x1; + int dx20 = y2 - y0, dy20 = x0 - x2; + + float bc0_start = edgeFunction(x1,y1, x2,y2, minX+0.5f,minY+0.5f); + float bc1_start = edgeFunction(x2,y2, x0,y0, minX+0.5f,minY+0.5f); + float bc2_start = edgeFunction(x0,y0, x1,y1, minX+0.5f,minY+0.5f); + + for (int y = minY; y <= maxY; y++, bc0_start += dy12, bc1_start += dy20, bc2_start += dy01) + { + float bc0 = bc0_start; + float bc1 = bc1_start; + float bc2 = bc2_start; + + for (int x = minX; x <= maxX; x++, bc0 += dx12, bc1 += dx20, bc2 += dx01) + { + float ic0 = bc0 * factor; + float ic1 = bc1 * factor; + float ic2 = bc2 * factor; + if (ic0 < 0 || ic1 < 0 || ic2 < 0) continue; + int db_index = y * db_stride + x; + + float w = 1 / (ic0 * w0 + ic1 * w1 + ic2 * w2); + float z = (ic0 * z0 + ic1 * z1 + ic2 * z2) * w; + + if (depthTest && (z < 0 || z > depthBuffer[db_index])) continue; + if (!colWrite) { + if (depthWrite) depthBuffer[db_index] = z; + continue; + } + + int R, G, B, A; + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + float u = (ic0 * u0 + ic1 * u1 + ic2 * u2) * w; + float v = (ic0 * v0 + ic1 * v1 + ic2 * v2) * w; + int texX = ((int)(Math_AbsF(u - FastFloor(u)) * curTexWidth )) & texWidthMask; + int texY = ((int)(Math_AbsF(v - FastFloor(v)) * curTexHeight)) & texHeightMask; + int texIndex = texY * curTexWidth + texX; + + BitmapCol tColor = curTexPixels[texIndex]; + int a1 = PackedCol_A(color), a2 = BitmapCol_A(tColor); + A = ( a1 * a2 ) >> 8; + int r1 = PackedCol_R(color), r2 = BitmapCol_R(tColor); + R = ( r1 * r2 ) >> 8; + int g1 = PackedCol_G(color), g2 = BitmapCol_G(tColor); + G = ( g1 * g2 ) >> 8; + int b1 = PackedCol_B(color), b2 = BitmapCol_B(tColor); + B = ( b1 * b2 ) >> 8; + } else { + R = PackedCol_R(color); + G = PackedCol_G(color); + B = PackedCol_B(color); + A = PackedCol_A(color); + } + + int cb_index = y * cb_stride + x; + if (gfx_alphaBlend) { + BitmapCol dst = colorBuffer[cb_index]; + int dstR = BitmapCol_R(dst); + int dstG = BitmapCol_G(dst); + int dstB = BitmapCol_B(dst); + + R = (R * A + dstR * (255 - A)) >> 8; + G = (G * A + dstG * (255 - A)) >> 8; + B = (B * A + dstB * (255 - A)) >> 8; + } + if (gfx_alphaTest && A < 0x80) continue; + + if (depthWrite) depthBuffer[db_index] = z; + colorBuffer[cb_index] = BitmapCol_Make(R, G, B, 0xFF); + } + } +} + +void DrawQuads(int startVertex, int verticesCount) { + Vertex vertices[4]; + int j = startVertex; + + if (gfx_rendering2D) { + // 4 vertices = 1 quad = 2 triangles + for (int i = 0; i < verticesCount / 4; i++, j += 4) + { + TransformVertex2D(j + 0, &vertices[0]); + TransformVertex2D(j + 1, &vertices[1]); + TransformVertex2D(j + 2, &vertices[2]); + TransformVertex2D(j + 3, &vertices[3]); + + DrawTriangle2D(&vertices[0], &vertices[2], &vertices[1]); + DrawTriangle2D(&vertices[2], &vertices[0], &vertices[3]); + } + } else { + // 4 vertices = 1 quad = 2 triangles + for (int i = 0; i < verticesCount / 4; i++, j += 4) + { + TransformVertex3D(j + 0, &vertices[0]); + TransformVertex3D(j + 1, &vertices[1]); + TransformVertex3D(j + 2, &vertices[2]); + TransformVertex3D(j + 3, &vertices[3]); + + DrawTriangle3D(&vertices[0], &vertices[2], &vertices[1]); + DrawTriangle3D(&vertices[2], &vertices[0], &vertices[3]); + } + } +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; +} + +void Gfx_DrawVb_Lines(int verticesCount) { } /* TODO */ + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + DrawQuads(startVertex, verticesCount); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + DrawQuads(0, verticesCount); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + DrawQuads(startVertex, verticesCount); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Other/Misc------------------------------------------------------* +*#########################################################################################################################*/ +static BitmapCol* CB_GetRow(struct Bitmap* bmp, int y, void* ctx) { + return colorBuffer + cb_stride * y; +} + +cc_result Gfx_TakeScreenshot(struct Stream* output) { + struct Bitmap bmp; + Bitmap_Init(bmp, fb_width, fb_height, NULL); + return Png_Encode(&bmp, output, CB_GetRow, false, NULL); +} + +cc_bool Gfx_WarnIfNecessary(void) { + return false; +} + +void Gfx_BeginFrame(void) { } + +void Gfx_EndFrame(void) { + Rect2D r = { 0, 0, fb_width, fb_height }; + Window_DrawFramebuffer(r, &fb_bmp); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_OnWindowResize(void) { + if (depthBuffer) DestroyBuffers(); + + fb_width = Game.Width; + fb_height = Game.Height; + + vp_hwidth = fb_width / 2.0f; + vp_hheight = fb_height / 2.0f; + + fb_maxX = fb_width - 1; + fb_maxY = fb_height - 1; + + Window_AllocFramebuffer(&fb_bmp, Game.Width, Game.Height); + colorBuffer = fb_bmp.scan0; + cb_stride = fb_bmp.width; + + depthBuffer = Mem_Alloc(fb_width * fb_height, 4, "depth buffer"); + db_stride = fb_width; +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } + +void Gfx_GetApiInfo(cc_string* info) { + int pointerSize = sizeof(void*) * 8; + String_Format1(info, "-- Using software (%i bit) --\n", &pointerSize); + PrintMaxTextureInfo(info); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } +#endif diff --git a/src/Graphics_WiiU.c b/src/Graphics_WiiU.c new file mode 100644 index 0000000..da1cce1 --- /dev/null +++ b/src/Graphics_WiiU.c @@ -0,0 +1,481 @@ +#include "Core.h" +#ifdef CC_BUILD_WIIU +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../build-wiiu/coloured_gsh.h" +#include "../build-wiiu/textured_gsh.h" + +static WHBGfxShaderGroup colorShader; +static WHBGfxShaderGroup textureShader; +static GX2Sampler sampler; +static GfxResourceID white_square; +static WHBGfxShaderGroup* group; + +static void InitGfx(void) { + GX2InitSampler(&sampler, GX2_TEX_CLAMP_MODE_WRAP, GX2_TEX_XY_FILTER_MODE_POINT); + + WHBGfxLoadGFDShaderGroup(&colorShader, 0, coloured_gsh); + WHBGfxInitShaderAttribute(&colorShader, "in_pos", 0, 0, GX2_ATTRIB_FORMAT_FLOAT_32_32_32); + WHBGfxInitShaderAttribute(&colorShader, "in_col", 0, 12, GX2_ATTRIB_FORMAT_UNORM_8_8_8_8); + WHBGfxInitFetchShader(&colorShader); + + WHBGfxLoadGFDShaderGroup(&textureShader, 0, textured_gsh); + WHBGfxInitShaderAttribute(&textureShader, "in_pos", 0, 0, GX2_ATTRIB_FORMAT_FLOAT_32_32_32); + WHBGfxInitShaderAttribute(&textureShader, "in_col", 0, 12, GX2_ATTRIB_FORMAT_UNORM_8_8_8_8); + WHBGfxInitShaderAttribute(&textureShader, "in_uv", 0, 16, GX2_ATTRIB_FORMAT_FLOAT_32_32); + WHBGfxInitFetchShader(&textureShader); +} + +void Gfx_Create(void) { + if (!Gfx.Created) InitGfx(); + + Gfx.Created = true; + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; +} + +cc_bool Gfx_TryRestoreContext(void) { + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + +static void Gfx_FreeState(void) { + FreeDefaultResources(); + Gfx_DeleteTexture(&white_square); +} + +static void Gfx_RestoreState(void) { + Gfx_SetFaceCulling(false); + InitDefaultResources(); + gfx_format = -1; + + // 1x1 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +static GX2Texture* pendingTex; + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + GX2Texture* tex = Mem_TryAllocCleared(1, sizeof(GX2Texture)); + if (!tex) return NULL; + + // TODO handle out of memory better + int width = bmp->width, height = bmp->height; + tex->surface.width = width; + tex->surface.height = height; + tex->surface.depth = 1; + tex->surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; + tex->surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; + tex->surface.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED; + tex->viewNumSlices = 1; + tex->compMap = GX2_COMP_MAP(GX2_SQ_SEL_R, GX2_SQ_SEL_G, GX2_SQ_SEL_B, GX2_SQ_SEL_A); + GX2CalcSurfaceSizeAndAlignment(&tex->surface); + GX2InitTextureRegs(tex); + + tex->surface.image = MEMAllocFromDefaultHeapEx(tex->surface.imageSize, tex->surface.alignment); + if (!tex->surface.image) { Mem_Free(tex); return NULL; } + + CopyTextureData(tex->surface.image, tex->surface.pitch << 2, bmp, rowWidth << 2); + GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, tex->surface.image, tex->surface.imageSize); + return tex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + GX2Texture* tex = (GX2Texture*)texId; + uint32_t* dst = (uint32_t*)tex->surface.image + (y * tex->surface.pitch) + x; + + CopyTextureData(dst, tex->surface.pitch << 2, part, rowWidth << 2); + GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, tex->surface.image, tex->surface.imageSize); +} + +void Gfx_BindTexture(GfxResourceID texId) { + if (!texId) texId = white_square; + pendingTex = (GX2Texture*)texId; + // Texture is bound to active shader, so might need to defer it in + // case a call to Gfx_BindTexture was called even though vertex format wasn't textured + // TODO: Track as dirty uniform flag instead? +} + +static void BindPendingTexture(void) { + if (!pendingTex || group != &textureShader) return; + + GX2SetPixelTexture(pendingTex, group->pixelShader->samplerVars[0].location); + GX2SetPixelSampler(&sampler, group->pixelShader->samplerVars[0].location); + pendingTex = NULL; +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + if (*texId == pendingTex) pendingTex = NULL; + // TODO free memory ??? +} + +void Gfx_EnableMipmaps(void) { } // TODO + +void Gfx_DisableMipmaps(void) { } // TODO + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static float clearR, clearG, clearB; +static cc_bool depthWrite = true, depthTest = true; + +static void UpdateDepthState(void) { + GX2SetDepthOnlyControl(depthTest, depthWrite, GX2_COMPARE_FUNC_LEQUAL); +} + +void Gfx_SetFaceCulling(cc_bool enabled) { + GX2SetCullOnlyControl(GX2_FRONT_FACE_CCW, false, enabled); +} + +void Gfx_SetFog(cc_bool enabled) { + // TODO +} + +void Gfx_SetFogCol(PackedCol color) { + // TODO +} + +void Gfx_SetFogDensity(float value) { + // TODO +} + +void Gfx_SetFogEnd(float value) { + // TODO +} + +void Gfx_SetFogMode(FogFunc func) { + // TODO +} + +static void SetAlphaTest(cc_bool enabled) { + GX2SetAlphaTest(enabled, GX2_COMPARE_FUNC_GEQUAL, 0.5f); +} + +static void SetAlphaBlend(cc_bool enabled) { + GX2SetBlendControl(GX2_RENDER_TARGET_0, + GX2_BLEND_MODE_SRC_ALPHA, GX2_BLEND_MODE_INV_SRC_ALPHA, GX2_BLEND_COMBINE_MODE_ADD, + true, + GX2_BLEND_MODE_SRC_ALPHA, GX2_BLEND_MODE_INV_SRC_ALPHA, GX2_BLEND_COMBINE_MODE_ADD); + GX2SetColorControl(GX2_LOGIC_OP_COPY, enabled, FALSE, TRUE); +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { +} + +void Gfx_ClearColor(PackedCol color) { + clearR = PackedCol_R(color) / 255.0f; + clearG = PackedCol_G(color) / 255.0f; + clearB = PackedCol_B(color) / 255.0f; +} + +void Gfx_SetDepthTest(cc_bool enabled) { + depthTest = enabled; + UpdateDepthState(); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + depthWrite = enabled; + UpdateDepthState(); +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + GX2ChannelMask mask = 0; + if (r) mask |= GX2_CHANNEL_MASK_R; + if (g) mask |= GX2_CHANNEL_MASK_G; + if (b) mask |= GX2_CHANNEL_MASK_B; + if (a) mask |= GX2_CHANNEL_MASK_A; + + // TODO: use GX2SetColorControl to disable all writing ??? + GX2SetTargetChannelMasks(mask, 0,0,0, 0,0,0,0); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; // don't need index buffers.. ? +} + +void Gfx_BindIb(GfxResourceID ib) { +} + +void Gfx_DeleteIb(GfxResourceID* ib) { +} + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + GX2RBuffer* buf = Mem_TryAllocCleared(1, sizeof(GX2RBuffer)); + if (!buf) return NULL; + + buf->flags = GX2R_RESOURCE_BIND_VERTEX_BUFFER | GX2R_RESOURCE_USAGE_CPU_READ | GX2R_RESOURCE_USAGE_CPU_WRITE | GX2R_RESOURCE_USAGE_GPU_READ; + buf->elemSize = strideSizes[fmt]; + buf->elemCount = count; + + if (GX2RCreateBuffer(buf)) return buf; + // Something went wrong ?? TODO + Mem_Free(buf); + return NULL; +} + +void Gfx_DeleteVb(GfxResourceID* vb) { + GX2RBuffer* buf = *vb; + if (!buf) return; + + GX2RDestroyBufferEx(buf, 0); + Mem_Free(buf); + *vb = NULL; +} + +void Gfx_BindVb(GfxResourceID vb) { + GX2RBuffer* buf = (GX2RBuffer*)vb; + GX2RSetAttributeBuffer(buf, 0, buf->elemSize, 0); + //GX2SetAttribBuffer(0, +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + GX2RBuffer* buf = (GX2RBuffer*)vb; + return GX2RLockBufferEx(buf, 0); +} + +void Gfx_UnlockVb(GfxResourceID vb) { + GX2RBuffer* buf = (GX2RBuffer*)vb; + GX2RUnlockBufferEx(buf, 0); +} + + +/*########################################################################################################################* +*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Gfx_AllocStaticVb(fmt, maxVertices); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return Gfx_LockVb(vb, fmt, count); +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { Gfx_UnlockVb(vb); Gfx_BindVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------Vertex rendering----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + group = fmt == VERTEX_FORMAT_TEXTURED ? &textureShader : &colorShader; + GX2SetFetchShader(&group->fetchShader); + GX2SetVertexShader(group->vertexShader); + GX2SetPixelShader(group->pixelShader); +} + +void Gfx_DrawVb_Lines(int verticesCount) { + BindPendingTexture(); + GX2DrawEx(GX2_PRIMITIVE_MODE_LINES, verticesCount, 0, 1); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + BindPendingTexture(); + GX2DrawEx(GX2_PRIMITIVE_MODE_QUADS, verticesCount, 0, 1); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + BindPendingTexture(); + GX2DrawEx(GX2_PRIMITIVE_MODE_QUADS, verticesCount, startVertex, 1); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + BindPendingTexture(); + GX2DrawEx(GX2_PRIMITIVE_MODE_QUADS, verticesCount, startVertex, 1); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj; +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + if (type == MATRIX_VIEW) _view = *matrix; + if (type == MATRIX_PROJECTION) _proj = *matrix; + + // TODO dirty uniform + struct Matrix mvp __attribute__((aligned(64))); + Matrix_Mul(&mvp, &_view, &_proj); + if (!group) return; + GX2SetVertexUniformReg(group->vertexShader->uniformVars[0].offset, 16, &mvp); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + // TODO +} + +void Gfx_DisableTextureOffset(void) { + // TODO +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // TODO verify this + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + // TODO verify this + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zNear - zFar); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; + + // TODO GX2SetSwapInterval(1); +} + +void Gfx_BeginFrame(void) { + uint32_t swapCount, flipCount; + OSTime lastFlip, lastVsync; + + for (int try = 0; try < 10; try++) + { + GX2GetSwapStatus(&swapCount, &flipCount, &lastFlip, &lastVsync); + if (flipCount >= swapCount) break; + GX2WaitForVsync(); // TODO vsync + } + + GX2ContextState* state = WHBGfxGetTVContextState(); + GX2SetContextState(state); +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + GX2ColorBuffer* buf = WHBGfxGetTVColourBuffer(); + GX2DepthBuffer* dph = WHBGfxGetTVDepthBuffer(); + + if (buffers & GFX_BUFFER_COLOR) { + GX2ClearColor(buf, clearR, clearG, clearB, 1.0f); + } + if (buffers & GFX_BUFFER_DEPTH) { + GX2ClearDepthStencilEx(dph, 1.0f, 0, GX2_CLEAR_FLAGS_DEPTH | GX2_CLEAR_FLAGS_STENCIL); + } +} + +static int drc_ticks; +void Gfx_EndFrame(void) { + GX2ColorBuffer* buf; + + buf = WHBGfxGetTVColourBuffer(); + GX2CopyColorBufferToScanBuffer(buf, GX2_SCAN_TARGET_TV); + + GX2ContextState* state = WHBGfxGetDRCContextState(); + GX2SetContextState(state); + drc_ticks = (drc_ticks + 1) % 200; + buf = WHBGfxGetDRCColourBuffer(); + GX2ClearColor(buf, drc_ticks / 200.0f, drc_ticks / 200.0f, drc_ticks / 200.0f, 1.0f); + GX2CopyColorBufferToScanBuffer(buf, GX2_SCAN_TARGET_DRC); + + GX2SwapScanBuffers(); + GX2Flush(); + GX2DrawDone(); + GX2SetTVEnable(TRUE); + GX2SetDRCEnable(TRUE); + + if (gfx_minFrameMs) LimitFPS(); +} + +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using Wii U --\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_OnWindowResize(void) { + +} + +void Gfx_SetViewport(int x, int y, int w, int h) { + GX2SetViewport(x, y, w, h, 0.0f, 1.0f); + GX2SetScissor( x, y, w, h); +} + +void Gfx_3DS_SetRenderScreen1(enum Screen3DS screen) { + GX2ContextState* tv_state = WHBGfxGetTVContextState(); + GX2ContextState* drc_state = WHBGfxGetDRCContextState(); // TODO + + GX2SetContextState(screen == TOP_SCREEN ? tv_state : drc_state); +} +#endif diff --git a/src/Graphics_Xbox.c b/src/Graphics_Xbox.c new file mode 100644 index 0000000..2d6712f --- /dev/null +++ b/src/Graphics_Xbox.c @@ -0,0 +1,701 @@ +#include "Core.h" +#if defined CC_BUILD_XBOX +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Logger.h" +#include "Window.h" +#include + +#define MAX_RAM_ADDR 0x03FFAFFF +#define MASK(mask, val) (((val) << (__builtin_ffs(mask)-1)) & (mask)) + +// https://github.com/XboxDev/nxdk/blob/master/samples/triangle/main.c +// https://xboxdevwiki.net/NV2A/Vertex_Shader#Output_registers +#define VERTEX_ATTR_INDEX 0 +#define COLOUR_ATTR_INDEX 3 +#define TEXTURE_ATTR_INDEX 9 + +// A lot of figuring out which GPU registers to use came from: +// - comparing against pbgl and pbkit + +static void LoadVertexShader(uint32_t* program, int programSize) { + uint32_t* p; + + // Set cursor for program upload + p = pb_begin(); + p = pb_push1(p, NV097_SET_TRANSFORM_PROGRAM_LOAD, 0); + pb_end(p); + + // Copy program instructions (16 bytes each) + for (int i = 0; i < programSize / 16; i++) + { + p = pb_begin(); + pb_push(p++, NV097_SET_TRANSFORM_PROGRAM, 4); + Mem_Copy(p, &program[i * 4], 4 * 4); + p += 4; + pb_end(p); + } +} + +static uint32_t vs_coloured_program[] = { + #include "../misc/xbox/vs_coloured.inl" +}; +static uint32_t vs_textured_program[] = { + #include "../misc/xbox/vs_textured.inl" +}; + + +static void LoadFragmentShader_Coloured(void) { + uint32_t* p; + + p = pb_begin(); + #include "../misc/xbox/ps_coloured.inl" + pb_end(p); +} + +static void LoadFragmentShader_Textured(void) { + uint32_t* p; + + p = pb_begin(); + #include "../misc/xbox/ps_textured.inl" + pb_end(p); +} + + +static void SetupShaders(void) { + uint32_t *p; + + p = pb_begin(); + // Set run address of shader + p = pb_push1(p, NV097_SET_TRANSFORM_PROGRAM_START, 0); + + // Set execution mode + p = pb_push1(p, NV097_SET_TRANSFORM_EXECUTION_MODE, + MASK(NV097_SET_TRANSFORM_EXECUTION_MODE_MODE, NV097_SET_TRANSFORM_EXECUTION_MODE_MODE_PROGRAM) + | MASK(NV097_SET_TRANSFORM_EXECUTION_MODE_RANGE_MODE, NV097_SET_TRANSFORM_EXECUTION_MODE_RANGE_MODE_PRIV)); + + p = pb_push1(p, NV097_SET_TRANSFORM_PROGRAM_CXT_WRITE_EN, 0); + + + // resets "z perspective" flag + //p = pb_push1(p, NV097_SET_CONTROL0, 0); + pb_end(p); +} + +static void ResetState(void) { + uint32_t* p = pb_begin(); + + p = pb_push1(p, NV097_SET_ALPHA_FUNC, 0x04); // GL_GREATER & 0x0F + p = pb_push1(p, NV097_SET_ALPHA_REF, 0x7F); + p = pb_push1(p, NV097_SET_DEPTH_FUNC, 0x03); // GL_LEQUAL & 0x0F + + p = pb_push1(p, NV097_SET_BLEND_FUNC_SFACTOR, NV097_SET_BLEND_FUNC_SFACTOR_V_SRC_ALPHA); + p = pb_push1(p, NV097_SET_BLEND_FUNC_DFACTOR, NV097_SET_BLEND_FUNC_DFACTOR_V_ONE_MINUS_SRC_ALPHA); + p = pb_push1(p, NV097_SET_BLEND_EQUATION, NV097_SET_BLEND_EQUATION_V_FUNC_ADD); // TODO not needed? + + p = pb_push1(p, NV097_SET_CULL_FACE, NV097_SET_CULL_FACE_V_FRONT); + // the order ClassiCube specifies quad vertices in are in the wrong order + // compared to what the GPU expects for front and back facing quads + + /*pb_push(p, NV097_SET_VERTEX_DATA_ARRAY_FORMAT, 16); p++; + for (int i = 0; i < 16; i++) + { + *(p++) = NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_F; + }*/ + pb_end(p); +} + +static GfxResourceID white_square; + +void Gfx_Create(void) { + Gfx.MaxTexWidth = 512; + Gfx.MaxTexHeight = 512; // TODO: 1024? + Gfx.Created = true; + + InitDefaultResources(); + pb_init(); + pb_show_front_screen(); + + SetupShaders(); + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + ResetState(); + + // 1x1 dummy white texture + struct Bitmap bmp; + BitmapCol pixels[1] = { BITMAPCOLOR_WHITE }; + Bitmap_Init(bmp, 1, 1, pixels); + white_square = Gfx_CreateTexture(&bmp, 0, false); +} + +void Gfx_Free(void) { + FreeDefaultResources(); + pb_kill(); +} + +cc_bool Gfx_TryRestoreContext(void) { return true; } +void Gfx_RestoreState(void) { } +void Gfx_FreeState(void) { } + + +/*########################################################################################################################* +*---------------------------------------------------------Texturing-------------------------------------------------------* +*#########################################################################################################################*/ +typedef struct CCTexture_ { + cc_uint32 width, height; + cc_uint32* pixels; +} CCTexture; + +// See Graphics_Dreamcast.c for twiddling explanation +static unsigned Interleave(unsigned x) { + // Simplified "Interleave bits by Binary Magic Numbers" from + // http://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN + + x = (x | (x << 8)) & 0x00FF00FF; + x = (x | (x << 4)) & 0x0F0F0F0F; + x = (x | (x << 2)) & 0x33333333; + x = (x | (x << 1)) & 0x55555555; + return x; +} + +#define Twiddle_CalcFactors(w, h) \ + min_dimension = min(w, h); \ + interleave_mask = min_dimension - 1; \ + interleaved_bits = Math_ilog2(min_dimension); \ + shifted_mask = 0xFFFFFFFFU & ~interleave_mask; \ + shift_bits = interleaved_bits; + +#define Twiddle_CalcY(y) \ + lo_Y = Interleave(y & interleave_mask) << 1; \ + hi_Y = (y & shifted_mask) << shift_bits; \ + Y = lo_Y | hi_Y; + +#define Twiddle_CalcX(x) \ + lo_X = Interleave(x & interleave_mask); \ + hi_X = (x & shifted_mask) << shift_bits; \ + X = lo_X | hi_X; + +static void ConvertTexture(cc_uint32* dst, struct Bitmap* bmp, int rowWidth) { + unsigned min_dimension; + unsigned interleave_mask, interleaved_bits; + unsigned shifted_mask, shift_bits; + unsigned lo_Y, hi_Y, Y; + unsigned lo_X, hi_X, X; + Twiddle_CalcFactors(bmp->width, bmp->height); + + for (int y = 0; y < bmp->height; y++) + { + Twiddle_CalcY(y); + cc_uint32* src = bmp->scan0 + y * rowWidth; + + for (int x = 0; x < bmp->width; x++, src++) + { + Twiddle_CalcX(x); + dst[X | Y] = *src; + } + } +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + int size = bmp->width * bmp->height * 4; + CCTexture* tex = Mem_Alloc(1, sizeof(CCTexture), "GPU texture"); + tex->pixels = MmAllocateContiguousMemoryEx(size, 0, MAX_RAM_ADDR, 0, PAGE_WRITECOMBINE | PAGE_READWRITE); + + tex->width = bmp->width; + tex->height = bmp->height; + ConvertTexture(tex->pixels, bmp, rowWidth); + return tex; +} + + +void Gfx_UpdateTexture(GfxResourceID texId, int originX, int originY, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + CCTexture* tex = (CCTexture*)texId; + cc_uint32* dst = tex->pixels; + + unsigned min_dimension; + unsigned interleave_mask, interleaved_bits; + unsigned shifted_mask, shift_bits; + unsigned lo_Y, hi_Y, Y; + unsigned lo_X, hi_X, X; + Twiddle_CalcFactors(tex->width, tex->height); + + for (int y = 0; y < part->height; y++) + { + int dstY = y + originY; + Twiddle_CalcY(dstY); + cc_uint32* src = part->scan0 + y * rowWidth; + + for (int x = 0; x < part->width; x++) + { + int dstX = x + originX; + Twiddle_CalcX(dstX); + dst[X | Y] = *src++; + } + } +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + CCTexture* tex = (CCTexture*)(*texId); + if (!tex) return; + + MmFreeContiguousMemory(tex->pixels); + Mem_Free(tex); + *texId = NULL; +} + +void Gfx_EnableMipmaps(void) { } +void Gfx_DisableMipmaps(void) { } + +void Gfx_BindTexture(GfxResourceID texId) { + CCTexture* tex = (CCTexture*)texId; + if (!tex) tex = white_square; + + unsigned log_u = Math_ilog2(tex->width); + unsigned log_v = Math_ilog2(tex->height); + uint32_t* p; + + p = pb_begin(); + // set texture stage 0 state + p = pb_push1(p, NV097_SET_TEXTURE_OFFSET, (DWORD)tex->pixels & 0x03ffffff); + p = pb_push1(p, NV097_SET_TEXTURE_FORMAT, + MASK(NV097_SET_TEXTURE_FORMAT_CONTEXT_DMA, 2) | + MASK(NV097_SET_TEXTURE_FORMAT_BORDER_SOURCE, NV097_SET_TEXTURE_FORMAT_BORDER_SOURCE_COLOR) | + MASK(NV097_SET_TEXTURE_FORMAT_COLOR, NV097_SET_TEXTURE_FORMAT_COLOR_SZ_A8R8G8B8) | + MASK(NV097_SET_TEXTURE_FORMAT_DIMENSIONALITY, 2) | // textures have U and V + MASK(NV097_SET_TEXTURE_FORMAT_MIPMAP_LEVELS, 1) | + MASK(NV097_SET_TEXTURE_FORMAT_BASE_SIZE_U, log_u) | + MASK(NV097_SET_TEXTURE_FORMAT_BASE_SIZE_V, log_v) | + MASK(NV097_SET_TEXTURE_FORMAT_BASE_SIZE_P, 0)); // log2(1) slice = 0 + p = pb_push1(p, NV097_SET_TEXTURE_CONTROL0, + NV097_SET_TEXTURE_CONTROL0_ENABLE | + MASK(NV097_SET_TEXTURE_CONTROL0_MIN_LOD_CLAMP, 0) | + MASK(NV097_SET_TEXTURE_CONTROL0_MAX_LOD_CLAMP, 1)); + p = pb_push1(p, NV097_SET_TEXTURE_ADDRESS, + 0x00010101); // modes (0x0W0V0U wrapping: 1=wrap 2=mirror 3=clamp 4=border 5=clamp to edge) + p = pb_push1(p, NV097_SET_TEXTURE_FILTER, + 0x2000 | + MASK(NV097_SET_TEXTURE_FILTER_MIN, 1) | + MASK(NV097_SET_TEXTURE_FILTER_MAG, 1)); // 1 = nearest filter + + // set texture matrix state + p = pb_push1(p, NV097_SET_TEXTURE_MATRIX_ENABLE, 0); + pb_end(p); +} + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +static PackedCol clearColor; + +void Gfx_ClearColor(PackedCol color) { + clearColor = color; +} + +void Gfx_SetFaceCulling(cc_bool enabled) { + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_CULL_FACE_ENABLE, enabled); + pb_end(p); +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { } + +static void SetAlphaBlend(cc_bool enabled) { + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_BLEND_ENABLE, enabled); + pb_end(p); +} + +static void SetAlphaTest(cc_bool enabled) { + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_ALPHA_TEST_ENABLE, enabled); + pb_end(p); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_DEPTH_MASK, enabled); + pb_end(p); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_DEPTH_TEST_ENABLE, enabled); + pb_end(p); +} + + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + unsigned mask = 0; + if (r) mask |= NV097_SET_COLOR_MASK_RED_WRITE_ENABLE; + if (g) mask |= NV097_SET_COLOR_MASK_GREEN_WRITE_ENABLE; + if (b) mask |= NV097_SET_COLOR_MASK_BLUE_WRITE_ENABLE; + if (a) mask |= NV097_SET_COLOR_MASK_ALPHA_WRITE_ENABLE; + + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_COLOR_MASK, mask); + pb_end(p); +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using XBox --\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { + pb_wait_for_vbl(); + pb_reset(); + pb_target_back_buffer(); +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + int width = pb_back_buffer_width(); + int height = pb_back_buffer_height(); + + // TODO do ourselves + if (buffers & GFX_BUFFER_DEPTH) + pb_erase_depth_stencil_buffer(0, 0, width, height); + if (buffers & GFX_BUFFER_COLOR) + pb_fill(0, 0, width, height, clearColor); + + //pb_erase_text_screen(); + while (pb_busy()) { } // Wait for completion TODO: necessary?? +} + +static int frames; +void Gfx_EndFrame(void) { + //pb_print("Frame #%d\n", frames++); + //pb_draw_text_screen(); + + while (pb_busy()) { } // Wait for frame completion + while (pb_finished()) { } // Swap when possible + + if (gfx_minFrameMs) LimitFPS(); +} + + +/*########################################################################################################################* +*----------------------------------------------------------Buffers--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint8* gfx_vertices; + +static void* AllocBuffer(int count, int elemSize) { + return MmAllocateContiguousMemoryEx(count * elemSize, 0, MAX_RAM_ADDR, 16, PAGE_WRITECOMBINE | PAGE_READWRITE); +} + +static void FreeBuffer(GfxResourceID* buffer) { + GfxResourceID ptr = *buffer; + if (ptr) MmFreeContiguousMemory(ptr); + *buffer = NULL; +} + + +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + return (void*)1; +} + +void Gfx_BindIb(GfxResourceID ib) { } + +void Gfx_DeleteIb(GfxResourceID* ib) { } + + +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + return AllocBuffer(count, strideSizes[fmt]); +} + +static uint32_t* PushAttribOffset(uint32_t* p, int index, cc_uint8* data) { + return pb_push1(p, NV097_SET_VERTEX_DATA_ARRAY_OFFSET + index * 4, + (uint32_t)data & 0x03ffffff); +} + +void Gfx_BindVb(GfxResourceID vb) { + gfx_vertices = vb; + uint32_t* p = pb_begin(); + + // TODO: Avoid the same code twice.. + if (gfx_format == VERTEX_FORMAT_TEXTURED) { + p = PushAttribOffset(p, VERTEX_ATTR_INDEX, gfx_vertices + 0); + p = PushAttribOffset(p, COLOUR_ATTR_INDEX, gfx_vertices + 12); + p = PushAttribOffset(p, TEXTURE_ATTR_INDEX, gfx_vertices + 16); + } else { + p = PushAttribOffset(p, VERTEX_ATTR_INDEX, gfx_vertices + 0); + p = PushAttribOffset(p, COLOUR_ATTR_INDEX, gfx_vertices + 12); + } + pb_end(p); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { FreeBuffer(vb); } + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { return vb; } + +void Gfx_UnlockVb(GfxResourceID vb) { } + + +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return AllocBuffer(maxVertices, strideSizes[fmt]); +} + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return vb; +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFog(cc_bool enabled) { +} + +void Gfx_SetFogCol(PackedCol color) { + int R = PackedCol_R(color); + int G = PackedCol_G(color); + int B = PackedCol_B(color); + int A = PackedCol_A(color); + + uint32_t* p = pb_begin(); + p = pb_push1(p, NV097_SET_FOG_COLOR, + MASK(NV097_SET_FOG_COLOR_RED, R) | + MASK(NV097_SET_FOG_COLOR_GREEN, G) | + MASK(NV097_SET_FOG_COLOR_BLUE, B) | + MASK(NV097_SET_FOG_COLOR_ALPHA, A)); + pb_end(p); +} + +void Gfx_SetFogDensity(float value) { +} + +void Gfx_SetFogEnd(float value) { +} + +void Gfx_SetFogMode(FogFunc func) { +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixorthooffcenterrh */ + /* The simplified calculation below uses: L = 0, R = width, T = 0, B = height */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +// https://github.com/XboxDev/nxdk/blob/master/samples/mesh/math3d.c#L292 +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + float zNear = 0.1f; + /* Source https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dxmatrixperspectivefovrh */ + /* NOTE: This calculation is shared with Direct3D 11 backend */ + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -(zFar + zNear) / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = -(2.0f * zFar * zNear) / (zFar - zNear); + matrix->row4.w = 0.0f; + // TODO: The above matrix breaks the held block + // Below works but breaks map rendering + +/* + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = -zFar / (zFar - zNear); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zFar - zNear); + matrix->row4.w = 0.0f;*/ +} + +void Gfx_OnWindowResize(void) { } + +static struct Vec4 vp_scale = { 320, -240, 8388608, 1 }; +static struct Vec4 vp_offset = { 320, 240, 8388608, 1 }; +static struct Matrix _view, _proj, _mvp; + +static void UpdateVSConstants(void) { + uint32_t* p; + p = pb_begin(); + + // resets "z perspective" flag + p = pb_push1(p, NV097_SET_CONTROL0, 0); + + // set shader constants cursor to C0 + p = pb_push1(p, NV097_SET_TRANSFORM_CONSTANT_LOAD, 96); + + // upload transformation matrix + pb_push(p++, NV097_SET_TRANSFORM_CONSTANT, 4*4 + 4 + 4); + Mem_Copy(p, &_mvp, 16 * 4); p += 16; + // Upload viewport too + Mem_Copy(p, &vp_scale, 4 * 4); p += 4; + Mem_Copy(p, &vp_offset, 4 * 4); p += 4; + // Upload constants too + //struct Vec4 v = { 1, 1, 1, 1 }; + //Mem_Copy(p, &v, 4 * 4); p += 4; + // if necessary, look at vs.inl output for 'c[5]' etc.. + + pb_end(p); +} + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + struct Matrix* dst = type == MATRIX_PROJECTION ? &_proj : &_view; + *dst = *matrix; + + Matrix_Mul(&_mvp, &_view, &_proj); + UpdateVSConstants(); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { +} + +void Gfx_DisableTextureOffset(void) { +} + +void Gfx_SetViewport(int x, int y, int w, int h) { + vp_scale.x = w * 0.5f; + vp_scale.y = h * -0.5f; + vp_offset.x = x + w * 0.5f; + vp_offset.y = y + h * 0.5f; + + uint32_t* p; + p = pb_begin(); + // NV097_SET_SURFACE_CLIP_HORIZONTAL followed by NV097_SET_SURFACE_CLIP_VERTICAL + p = pb_push2(p, NV097_SET_SURFACE_CLIP_HORIZONTAL, x | (w << 16), y | (h << 16)); + pb_end(p); +} + + + + +/*########################################################################################################################* +*---------------------------------------------------------Drawing---------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +static uint32_t* PushAttrib(uint32_t* p, int index, int format, int size, int stride) { + return pb_push1(p, NV097_SET_VERTEX_DATA_ARRAY_FORMAT + index * 4, + MASK(NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE, format) | + MASK(NV097_SET_VERTEX_DATA_ARRAY_FORMAT_SIZE, size) | + MASK(NV097_SET_VERTEX_DATA_ARRAY_FORMAT_STRIDE, stride)); +} + +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + uint32_t* p = pb_begin(); + // Clear all attributes TODO optimise + pb_push(p++, NV097_SET_VERTEX_DATA_ARRAY_FORMAT,16); + for (int i = 0; i < 16; i++) + { + *(p++) = NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_F; + } + + // resets "z perspective" flag + //p = pb_push1(p, NV097_SET_CONTROL0, 0); + + // TODO cache these.. + if (fmt == VERTEX_FORMAT_TEXTURED) { + p = PushAttrib(p, VERTEX_ATTR_INDEX, NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_F, + 3, SIZEOF_VERTEX_TEXTURED); + p = PushAttrib(p, COLOUR_ATTR_INDEX, NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_UB_D3D, + 4, SIZEOF_VERTEX_TEXTURED); + p = PushAttrib(p, TEXTURE_ATTR_INDEX, NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_F, + 2, SIZEOF_VERTEX_TEXTURED); + } else { + p = PushAttrib(p, VERTEX_ATTR_INDEX, NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_F, + 3, SIZEOF_VERTEX_COLOURED); + p = PushAttrib(p, COLOUR_ATTR_INDEX, NV097_SET_VERTEX_DATA_ARRAY_FORMAT_TYPE_UB_D3D, + 4, SIZEOF_VERTEX_COLOURED); + } + pb_end(p); + + if (fmt == VERTEX_FORMAT_TEXTURED) { + LoadVertexShader(vs_textured_program, sizeof(vs_textured_program)); + LoadFragmentShader_Textured(); + } else { + LoadVertexShader(vs_coloured_program, sizeof(vs_coloured_program)); + LoadFragmentShader_Coloured(); + } +} + +static void DrawArrays(int mode, int start, int count) { + uint32_t *p = pb_begin(); + p = pb_push1(p, NV097_SET_BEGIN_END, mode); + + // NV097_DRAW_ARRAYS_COUNT is an 8 bit mask, so must be <= 256 + while (count > 0) + { + int batch_count = min(count, 256); + + p = pb_push1(p, 0x40000000 | NV097_DRAW_ARRAYS, + MASK(NV097_DRAW_ARRAYS_COUNT, (batch_count-1)) | + MASK(NV097_DRAW_ARRAYS_START_INDEX, start)); + + start += batch_count; + count -= batch_count; + } + + p = pb_push1(p, NV097_SET_BEGIN_END, NV097_SET_BEGIN_END_OP_END); + pb_end(p); +} + +void Gfx_DrawVb_Lines(int verticesCount) { + DrawArrays(NV097_SET_BEGIN_END_OP_LINES, 0, verticesCount); +} + +static void DrawIndexedVertices(int verticesCount, int startVertex) { + DrawArrays(NV097_SET_BEGIN_END_OP_QUADS, startVertex, verticesCount); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + DrawIndexedVertices(verticesCount, startVertex); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + DrawIndexedVertices(verticesCount, 0); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + DrawIndexedVertices(verticesCount, startVertex); +} +#endif diff --git a/src/Graphics_Xbox360.c b/src/Graphics_Xbox360.c new file mode 100644 index 0000000..71a4efb --- /dev/null +++ b/src/Graphics_Xbox360.c @@ -0,0 +1,407 @@ +#include "Core.h" +#ifdef CC_BUILD_XBOX360 +#include "_GraphicsBase.h" +#include "Errors.h" +#include "Window.h" +#include +#include + +#include "../misc/xbox360/ps_coloured.h" +#include "../misc/xbox360/vs_coloured.h" +#include "../misc/xbox360/ps_textured.h" +#include "../misc/xbox360/vs_textured.h" +static struct XenosShader* shdr_tex_vs; +static struct XenosShader* shdr_tex_ps; +static struct XenosShader* shdr_col_vs; +static struct XenosShader* shdr_col_ps; + +static struct XenosDevice device; +static struct XenosDevice* xe; + +static const struct XenosVBFFormat textured_vbf = { +3, { + { XE_USAGE_POSITION, 0, XE_TYPE_FLOAT4 }, + { XE_USAGE_COLOR, 0, XE_TYPE_UBYTE4 }, + { XE_USAGE_TEXCOORD, 0, XE_TYPE_FLOAT2 } +} }; + +static const struct XenosVBFFormat coloured_vbf = { +2, { + { XE_USAGE_POSITION, 0, XE_TYPE_FLOAT4 }, + { XE_USAGE_COLOR, 0, XE_TYPE_UBYTE4 } +} }; + +static void CreateState(void) { + xe = &device; + Xe_Init(xe); + edram_init(xe); + + struct XenosSurface* fb = Xe_GetFramebufferSurface(xe); + Xe_SetRenderTarget(xe, fb); +} + +static void CreateShaders(void) { + shdr_tex_vs = Xe_LoadShaderFromMemory(xe, (void*)vs_textured); + Xe_InstantiateShader(xe, shdr_tex_vs, 0); + Xe_ShaderApplyVFetchPatches(xe, shdr_tex_vs, 0, &textured_vbf); + shdr_tex_ps = Xe_LoadShaderFromMemory(xe, (void*)ps_textured); + Xe_InstantiateShader(xe, shdr_tex_ps, 0); + + shdr_col_vs = Xe_LoadShaderFromMemory(xe, (void*)vs_coloured); + Xe_InstantiateShader(xe, shdr_col_vs, 0); + Xe_ShaderApplyVFetchPatches(xe, shdr_col_vs, 0, &coloured_vbf); + shdr_col_ps = Xe_LoadShaderFromMemory(xe, (void*)ps_coloured); + Xe_InstantiateShader(xe, shdr_col_ps, 0); +} + +void Gfx_Create(void) { + if (!Gfx.Created) { + CreateState(); + CreateShaders(); + } + Gfx.Created = true; + Gfx_SetVertexFormat(VERTEX_FORMAT_COLOURED); + + Gfx.MaxTexWidth = 1024; + Gfx.MaxTexHeight = 1024; + Gfx.MaxTexSize = 512 * 512; +} + +cc_bool Gfx_TryRestoreContext(void) { + return true; +} + +void Gfx_Free(void) { + Gfx_FreeState(); +} + +static void Gfx_FreeState(void) { + FreeDefaultResources(); +} + +static void Gfx_RestoreState(void) { + InitDefaultResources(); + Gfx_SetFaceCulling(false); + SetAlphaBlend(false); + + Xe_SetAlphaFunc(xe, XE_CMP_GREATER); + Xe_SetAlphaRef(xe, 0.5f); + Xe_SetZFunc(xe, XE_CMP_LESSEQUAL); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Textures--------------------------------------------------------* +*#########################################################################################################################*/ +static void SetTextureData(struct XenosSurface* xtex, int x, int y, const struct Bitmap* bmp, int rowWidth, int lvl) { + void* dst = Xe_Surface_LockRect(xe, xtex, x, y, bmp->width, bmp->height, XE_LOCK_WRITE); + + CopyTextureData(dst, bmp->width * 4, bmp, rowWidth << 2); + + Xe_Surface_Unlock(xe, xtex); +} + +static GfxResourceID Gfx_AllocTexture(struct Bitmap* bmp, int rowWidth, cc_uint8 flags, cc_bool mipmaps) { + struct XenosSurface* xtex = Xe_CreateTexture(xe, bmp->width, bmp->height, 1, XE_FMT_8888, 0); + SetTextureData(xtex, 0, 0, bmp, rowWidth, 0); + return xtex; +} + +void Gfx_UpdateTexture(GfxResourceID texId, int x, int y, struct Bitmap* part, int rowWidth, cc_bool mipmaps) { + struct XenosSurface* xtex = (struct XenosSurface*)texId; + SetTextureData(xtex, x, y, part, rowWidth, 0); +} + +void Gfx_BindTexture(GfxResourceID texId) { + struct XenosSurface* xtex = (struct XenosSurface*)texId; + Xe_SetTexture(xe, 0, xtex); +} + +void Gfx_DeleteTexture(GfxResourceID* texId) { + struct XenosSurface* xtex = (struct XenosSurface*)(*texId); + if (xtex) Xe_DestroyTexture(xe, xtex); + *texId = NULL; +} + +void Gfx_EnableMipmaps(void) { } // TODO + +void Gfx_DisableMipmaps(void) { } // TODO + + +/*########################################################################################################################* +*-----------------------------------------------------State management----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetFaceCulling(cc_bool enabled) { + Xe_SetCullMode(xe, enabled ? XE_CULL_CW : XE_CULL_NONE); +} + +void Gfx_SetFog(cc_bool enabled) { + // TODO +} + +void Gfx_SetFogCol(PackedCol color) { + // TODO +} + +void Gfx_SetFogDensity(float value) { + // TODO +} + +void Gfx_SetFogEnd(float value) { + // TODO +} + +void Gfx_SetFogMode(FogFunc func) { + // TODO +} + +static void SetAlphaTest(cc_bool enabled) { + Xe_SetAlphaTestEnable(xe, enabled); +} + +static void SetAlphaBlend(cc_bool enabled) { + if (enabled) { + Xe_SetBlendControl(xe, + XE_BLEND_SRCALPHA, XE_BLENDOP_ADD, XE_BLEND_INVSRCALPHA, + XE_BLEND_SRCALPHA, XE_BLENDOP_ADD, XE_BLEND_INVSRCALPHA); + } else { + Xe_SetBlendControl(xe, + XE_BLEND_ONE, XE_BLENDOP_ADD, XE_BLEND_ZERO, + XE_BLEND_ONE, XE_BLENDOP_ADD, XE_BLEND_ZERO); + } +} + +void Gfx_SetAlphaArgBlend(cc_bool enabled) { + // TODO +} + +void Gfx_ClearColor(PackedCol color) { + Xe_SetClearColor(xe, color); +} + +void Gfx_SetDepthTest(cc_bool enabled) { + Xe_SetZEnable(xe, enabled); +} + +void Gfx_SetDepthWrite(cc_bool enabled) { + Xe_SetZWrite(xe, enabled); +} + +static void SetColorWrite(cc_bool r, cc_bool g, cc_bool b, cc_bool a) { + // TODO +} + +void Gfx_DepthOnlyRendering(cc_bool depthOnly) { + cc_bool enabled = !depthOnly; + SetColorWrite(enabled & gfx_colorMask[0], enabled & gfx_colorMask[1], + enabled & gfx_colorMask[2], enabled & gfx_colorMask[3]); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Index buffers-----------------------------------------------------* +*#########################################################################################################################*/ +GfxResourceID Gfx_CreateIb2(int count, Gfx_FillIBFunc fillFunc, void* obj) { + int size = count * 2; + struct XenosIndexBuffer* xib = Xe_CreateIndexBuffer(xe, size, XE_FMT_INDEX16); + + void* dst = Xe_IB_Lock(xe, xib, 0, size, XE_LOCK_WRITE); + fillFunc((cc_uint16*)dst, count, obj); + Xe_IB_Unlock(xe, xib); + return xib; +} + +void Gfx_BindIb(GfxResourceID ib) { + struct XenosIndexBuffer* xib = (struct XenosIndexBuffer*)ib; + Xe_SetIndices(xe, xib); +} + +void Gfx_DeleteIb(GfxResourceID* ib) { + struct XenosIndexBuffer* xib = (struct XenosIndexBuffer*)(*ib); + if (xib) Xe_DestroyIndexBuffer(xe, xib); + *ib = NULL; +} + + +/*########################################################################################################################* +*------------------------------------------------------Vertex buffers-----------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocStaticVb(VertexFormat fmt, int count) { + int size = count * strideSizes[fmt]; + return Xe_CreateVertexBuffer(xe, size); +} + +void Gfx_DeleteVb(GfxResourceID* vb) { + struct XenosVertexBuffer* xvb = (struct XenosVertexBuffer*)(*vb); + if (xvb) Xe_DestroyVertexBuffer(xe, xvb); + *vb = NULL; +} + +void Gfx_BindVb(GfxResourceID vb) { + struct XenosVertexBuffer* xvb = (struct XenosVertexBuffer*)vb; + Xe_SetStreamSource(xe, 0, xvb, 0, gfx_stride); +} + +void* Gfx_LockVb(GfxResourceID vb, VertexFormat fmt, int count) { + struct XenosVertexBuffer* xvb = (struct XenosVertexBuffer*)vb; + int size = count * strideSizes[fmt]; + return Xe_VB_Lock(xe, xvb, 0, size, XE_LOCK_WRITE); +} + +void Gfx_UnlockVb(GfxResourceID vb) { + struct XenosVertexBuffer* xvb = (struct XenosVertexBuffer*)vb; + Xe_VB_Unlock(xe, xvb); +} + + +/*########################################################################################################################* +*--------------------------------------------------Dynamic vertex buffers-------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID Gfx_AllocDynamicVb(VertexFormat fmt, int maxVertices) { + return Gfx_AllocStaticVb(fmt, maxVertices); +} + +void Gfx_DeleteDynamicVb(GfxResourceID* vb) { Gfx_DeleteVb(vb); } + +void Gfx_BindDynamicVb(GfxResourceID vb) { Gfx_BindVb(vb); } + +void* Gfx_LockDynamicVb(GfxResourceID vb, VertexFormat fmt, int count) { + return Gfx_LockVb(vb, fmt, count); +} + +void Gfx_UnlockDynamicVb(GfxResourceID vb) { Gfx_UnlockVb(vb); Gfx_BindVb(vb); } + + +/*########################################################################################################################* +*-----------------------------------------------------Vertex rendering----------------------------------------------------* +*#########################################################################################################################*/ +void Gfx_SetVertexFormat(VertexFormat fmt) { + if (fmt == gfx_format) return; + gfx_format = fmt; + gfx_stride = strideSizes[fmt]; + + if (fmt == VERTEX_FORMAT_COLOURED) { + Xe_SetTexture(xe, 0, NULL); + Xe_SetShader(xe, SHADER_TYPE_PIXEL, shdr_col_ps, 0); + Xe_SetShader(xe, SHADER_TYPE_VERTEX, shdr_col_vs, 0); + } else { + Xe_SetShader(xe, SHADER_TYPE_PIXEL, shdr_tex_ps, 0); + Xe_SetShader(xe, SHADER_TYPE_VERTEX, shdr_tex_vs, 0); + } +} + +void Gfx_DrawVb_Lines(int verticesCount) { + Xe_DrawPrimitive(xe, XE_PRIMTYPE_LINELIST, 0, verticesCount >> 1); +} + +void Gfx_DrawVb_IndexedTris(int verticesCount) { + Xe_DrawIndexedPrimitive(xe, XE_PRIMTYPE_TRIANGLELIST, // TODO QUADLIST instead? + 0, 0, verticesCount, 0, verticesCount >> 1); +} + +void Gfx_DrawVb_IndexedTris_Range(int verticesCount, int startVertex) { + Xe_DrawIndexedPrimitive(xe, XE_PRIMTYPE_TRIANGLELIST, + startVertex, 0, verticesCount, 0, verticesCount >> 1); +} + +void Gfx_DrawIndexedTris_T2fC4b(int verticesCount, int startVertex) { + Xe_DrawIndexedPrimitive(xe, XE_PRIMTYPE_TRIANGLELIST, + startVertex, 0, verticesCount, 0, verticesCount >> 1); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Matrices--------------------------------------------------------* +*#########################################################################################################################*/ +static struct Matrix _view, _proj, _mvp; + +void Gfx_LoadMatrix(MatrixType type, const struct Matrix* matrix) { + struct Matrix* dst = type == MATRIX_PROJECTION ? &_proj : &_view; + *dst = *matrix; + + Matrix_Mul(&_mvp, &_view, &_proj); + // TODO: Is this a global uniform, or does it need to be reloaded on shader change? + Xe_SetVertexShaderConstantF(xe, 0, (float*)&_mvp, 4); +} + +void Gfx_LoadIdentityMatrix(MatrixType type) { + Gfx_LoadMatrix(type, &Matrix_Identity); +} + +void Gfx_EnableTextureOffset(float x, float y) { + // TODO +} + +void Gfx_DisableTextureOffset(void) { + // TODO +} + +void Gfx_CalcOrthoMatrix(struct Matrix* matrix, float width, float height, float zNear, float zFar) { + // TODO verify this + *matrix = Matrix_Identity; + + matrix->row1.x = 2.0f / width; + matrix->row2.y = -2.0f / height; + matrix->row3.z = 1.0f / (zNear - zFar); + + matrix->row4.x = -1.0f; + matrix->row4.y = 1.0f; + matrix->row4.z = zNear / (zNear - zFar); +} + +static float Cotangent(float x) { return Math_CosF(x) / Math_SinF(x); } +void Gfx_CalcPerspectiveMatrix(struct Matrix* matrix, float fov, float aspect, float zFar) { + // TODO verify this + float zNear = 0.1f; + float c = Cotangent(0.5f * fov); + *matrix = Matrix_Identity; + + matrix->row1.x = c / aspect; + matrix->row2.y = c; + matrix->row3.z = zFar / (zNear - zFar); + matrix->row3.w = -1.0f; + matrix->row4.z = (zNear * zFar) / (zNear - zFar); + matrix->row4.w = 0.0f; +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Misc----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Gfx_TakeScreenshot(struct Stream* output) { + return ERR_NOT_SUPPORTED; +} + +void Gfx_SetFpsLimit(cc_bool vsync, float minFrameMs) { + gfx_minFrameMs = minFrameMs; + gfx_vsync = vsync; +} + +void Gfx_BeginFrame(void) { +} + +void Gfx_ClearBuffers(GfxBuffers buffers) { + // TODO clear only some buffers + // Xe_Clear is just a Resolve anyways +} + +void Gfx_EndFrame(void) { + Xe_Resolve(xe); + Xe_Sync(xe); + + if (gfx_minFrameMs) LimitFPS(); +} + +cc_bool Gfx_WarnIfNecessary(void) { return false; } + +void Gfx_GetApiInfo(cc_string* info) { + String_AppendConst(info, "-- Using XBox 360 --\n"); + PrintMaxTextureInfo(info); +} + +void Gfx_OnWindowResize(void) { + +} + +void Gfx_SetViewport(int x, int y, int w, int h) { } +#endif diff --git a/src/Gui.c b/src/Gui.c new file mode 100644 index 0000000..eefd01e --- /dev/null +++ b/src/Gui.c @@ -0,0 +1,731 @@ +#include "Gui.h" +#include "String.h" +#include "Window.h" +#include "Game.h" +#include "Graphics.h" +#include "Event.h" +#include "Drawer2D.h" +#include "ExtMath.h" +#include "Screens.h" +#include "Camera.h" +#include "Input.h" +#include "Logger.h" +#include "Platform.h" +#include "Bitmap.h" +#include "Options.h" +#include "Menus.h" +#include "Funcs.h" +#include "Server.h" +#include "TexturePack.h" + +struct _GuiData Gui; +struct Screen* Gui_Screens[GUI_MAX_SCREENS]; +static cc_uint8 priorities[GUI_MAX_SCREENS]; +#ifdef CC_BUILD_DUALSCREEN +static struct Texture touchBgTex; +#endif + +/*########################################################################################################################* +*----------------------------------------------------------Gui------------------------------------------------------------* +*#########################################################################################################################*/ +static CC_NOINLINE int GetWindowScale(void) { + float widthScale = Window_Main.Width / 640.0f; + float heightScale = Window_Main.Height / 480.0f; + + /* Use larger UI scaling on mobile */ + /* TODO move this DPI scaling elsewhere.,. */ +#ifndef CC_BUILD_DUALSCREEN + if (!Gui_TouchUI) { +#endif + widthScale /= DisplayInfo.ScaleX; + heightScale /= DisplayInfo.ScaleY; +#ifndef CC_BUILD_DUALSCREEN + } +#endif + return 1 + (int)(min(widthScale, heightScale)); +} + +float Gui_Scale(float value) { + return (float)((int)(value * 10 + 0.5f)) / 10.0f; +} + +float Gui_GetHotbarScale(void) { + return Gui_Scale(GetWindowScale() * Gui.RawHotbarScale); +} + +float Gui_GetInventoryScale(void) { + return Gui_Scale(GetWindowScale() * (Gui.RawInventoryScale * 0.5f)); +} + +float Gui_GetChatScale(void) { + if (Gui.AutoScaleChat) return Gui_Scale(GetWindowScale() * Gui.RawChatScale); + return Gui.RawChatScale; +} + +float Gui_GetCrosshairScale(void) { + return Gui_Scale((Window_Main.Height / 480.0f)) * Gui.RawCrosshairScale; +} + + +void Gui_MakeTitleFont(struct FontDesc* font) { Font_Make(font, 16, FONT_FLAGS_BOLD); } +void Gui_MakeBodyFont(struct FontDesc* font) { Font_Make(font, 16, FONT_FLAGS_NONE); } + +int Gui_CalcPos(cc_uint8 anchor, int offset, int size, int axisLen) { + if (anchor == ANCHOR_MIN) return offset; + if (anchor == ANCHOR_MAX) return axisLen - size - offset; + + if (anchor == ANCHOR_CENTRE_MIN) return (axisLen / 2) + offset; + if (anchor == ANCHOR_CENTRE_MAX) return (axisLen / 2) - size - offset; + return (axisLen - size) / 2 + offset; +} + +int Gui_Contains(int recX, int recY, int width, int height, int x, int y) { + return x >= recX && y >= recY && x < (recX + width) && y < (recY + height); +} + +int Gui_ContainsPointers(int x, int y, int width, int height) { + int i, px, py; + for (i = 0; i < Pointers_Count; i++) + { + px = Pointers[i].x; py = Pointers[i].y; + + if (px >= x && py >= y && px < (x + width) && py < (y + height)) return true; + } + return false; +} + +void Gui_ShowDefault(void) { + HUDScreen_Show(); + ChatScreen_Show(); +#ifdef CC_BUILD_TOUCH + TouchScreen_Show(); +#endif +} + +#ifdef CC_BUILD_TOUCH +void Gui_SetTouchUI(cc_bool enabled) { + Gui.TouchUI = enabled; /* TODO toggle or not */ +} +#endif + +static void LoadOptions(void) { + Gui.DefaultLines = Game_ClassicMode ? 10 : 12; + Gui.Chatlines = Options_GetInt(OPT_CHATLINES, 0, GUI_MAX_CHATLINES, Gui.DefaultLines); + Gui.ClickableChat = !Game_ClassicMode && Options_GetBool(OPT_CLICKABLE_CHAT, !Input_TouchMode); + Gui.TabAutocomplete = !Game_ClassicMode && Options_GetBool(OPT_TAB_AUTOCOMPLETE, true); + + Gui.ClassicTexture = Options_GetBool(OPT_CLASSIC_GUI, true) || Game_ClassicMode; + Gui.ClassicTabList = Options_GetBool(OPT_CLASSIC_TABLIST, false) || Game_ClassicMode; + Gui.ClassicMenu = Options_GetBool(OPT_CLASSIC_OPTIONS, false) || Game_ClassicMode; + Gui.ClassicChat = Options_GetBool(OPT_CLASSIC_CHAT, false) || Game_PureClassic; + Gui.ClassicInventory = Options_GetBool(OPT_CLASSIC_INVENTORY, false) || Game_ClassicMode; + Gui.ShowFPS = Options_GetBool(OPT_SHOW_FPS, true); + + Gui.RawInventoryScale = Options_GetFloat(OPT_INVENTORY_SCALE, 0.25f, 5.0f, 1.0f); + Gui.RawHotbarScale = Options_GetFloat(OPT_HOTBAR_SCALE, 0.25f, 5.0f, 1.0f); + Gui.RawChatScale = Options_GetFloat(OPT_CHAT_SCALE, 0.25f, 5.0f, 1.0f); + Gui.RawCrosshairScale = Options_GetFloat(OPT_CROSSHAIR_SCALE, 0.25f, 5.0f, 1.0f); + Gui.RawTouchScale = Options_GetFloat(OPT_TOUCH_SCALE, 0.25f, 5.0f, 1.0f); + + Gui.AutoScaleChat = Options_GetBool(OPT_CHAT_AUTO_SCALE, true); +} + +static void LoseAllScreens(void) { + struct Screen* s; + int i; + + for (i = 0; i < Gui.ScreensCount; i++) + { + s = Gui_Screens[i]; + s->VTABLE->ContextLost(s); + } +} + +static void OnContextRecreated(void* obj) { + struct Screen* s; + int i; + + for (i = 0; i < Gui.ScreensCount; i++) + { + s = Gui_Screens[i]; + s->VTABLE->ContextRecreated(s); + s->dirty = true; + } +} + +static void OnResize(void* obj) { Gui_LayoutAll(); } +void Gui_LayoutAll(void) { + struct Screen* s; + int i; + + for (i = 0; i < Gui.ScreensCount; i++) + { + s = Gui_Screens[i]; + s->VTABLE->Layout(s); + s->dirty = true; + } +} + +void Gui_RefreshAll(void) { + LoseAllScreens(); + OnContextRecreated(NULL); + OnResize(NULL); +} + +void Gui_Refresh(struct Screen* s) { + s->VTABLE->ContextLost(s); + s->VTABLE->ContextRecreated(s); + s->VTABLE->Layout(s); + s->dirty = true; +} + +static void Gui_AddCore(struct Screen* s, int priority) { + int i, j; + if (Gui.ScreensCount >= GUI_MAX_SCREENS) Logger_Abort("Hit max screens"); + + for (i = 0; i < Gui.ScreensCount; i++) + { + if (priority <= priorities[i]) continue; + + /* Shift lower priority screens right */ + for (j = Gui.ScreensCount; j > i; j--) + { + Gui_Screens[j] = Gui_Screens[j - 1]; + priorities[j] = priorities[j - 1]; + } + break; + } + + Gui_Screens[i] = s; + priorities[i] = priority; + Gui.ScreensCount++; + + s->dirty = true; + s->VTABLE->Init(s); + s->VTABLE->ContextRecreated(s); + s->VTABLE->Layout(s); + + /* for selecting active button etc */ + for (i = 0; i < Pointers_Count; i++) + { + s->VTABLE->HandlesPointerMove(s, i, Pointers[i].x, Pointers[i].y); + } +} + +/* Returns index of the given screen in the screens list, -1 if not */ +static int IndexOfScreen(struct Screen* s) { + int i; + for (i = 0; i < Gui.ScreensCount; i++) + { + if (Gui_Screens[i] == s) return i; + } + return -1; +} + +void Gui_RemoveCore(struct Screen* s) { + int i = IndexOfScreen(s); + if (i == -1) return; + + for (; i < Gui.ScreensCount - 1; i++) + { + Gui_Screens[i] = Gui_Screens[i + 1]; + priorities[i] = priorities[i + 1]; + } + Gui.ScreensCount--; + + s->VTABLE->ContextLost(s); + s->VTABLE->Free(s); +} + +CC_NOINLINE static void Gui_OnScreensChanged(void) { + Gui_UpdateInputGrab(); + InputHandler_OnScreensChanged(); +} + +void Gui_Remove(struct Screen* s) { + Gui_RemoveCore(s); + Gui_OnScreensChanged(); +} + +void Gui_Add(struct Screen* s, int priority) { + struct Screen* existing; + Gui_RemoveCore(s); + + existing = Gui_GetScreen(priority); + if (existing) Gui_RemoveCore(existing); + + Gui_AddCore(s, priority); + Gui_OnScreensChanged(); +} + +struct Screen* Gui_GetInputGrab(void) { + int i; + for (i = 0; i < Gui.ScreensCount; i++) + { + if (Gui_Screens[i]->grabsInput) return Gui_Screens[i]; + } + return NULL; +} + +struct Screen* Gui_GetBlocksWorld(void) { + int i; + for (i = 0; i < Gui.ScreensCount; i++) + { + if (Gui_Screens[i]->blocksWorld) return Gui_Screens[i]; + } + return NULL; +} + +struct Screen* Gui_GetClosable(void) { + int i; + for (i = 0; i < Gui.ScreensCount; i++) + { + if (Gui_Screens[i]->closable) return Gui_Screens[i]; + } + return NULL; +} + +struct Screen* Gui_GetScreen(int priority) { + int i; + for (i = 0; i < Gui.ScreensCount; i++) + { + if (priorities[i] == priority) return Gui_Screens[i]; + } + return NULL; +} + +void Gui_UpdateInputGrab(void) { + Gui.InputGrab = Gui_GetInputGrab(); + Camera_CheckFocus(); +} + +void Gui_ShowPauseMenu(void) { + if (Gui.ClassicMenu) { + ClassicPauseScreen_Show(); + } else { + PauseScreen_Show(); + } +} + +void Gui_RenderGui(float delta) { + struct Screen* s; + int i; + + Gfx_3DS_SetRenderScreen(BOTTOM_SCREEN); +#ifdef CC_BUILD_DUALSCREEN + Texture_Render(&touchBgTex); +#endif + + /* Draw back to front so highest priority screen is on top */ + for (i = Gui.ScreensCount - 1; i >= 0; i--) + { + s = Gui_Screens[i]; + s->VTABLE->Update(s, delta); + + if (s->dirty) { s->VTABLE->BuildMesh(s); s->dirty = false; } + s->VTABLE->Render(s, delta); + } + + Gfx_3DS_SetRenderScreen(TOP_SCREEN); +} + + +/*########################################################################################################################* +*-------------------------------------------------------TextAtlas---------------------------------------------------------* +*#########################################################################################################################*/ +void TextAtlas_Make(struct TextAtlas* atlas, const cc_string* chars, struct FontDesc* font, const cc_string* prefix) { + struct DrawTextArgs args; + struct Context2D ctx; + int width, height; + int i, charWidth; + + Gfx_DeleteTexture(&atlas->tex.ID); + DrawTextArgs_Make(&args, prefix, font, true); + width = Drawer2D_TextWidth(&args); + atlas->offset = width; + + for (i = 0; i < chars->length; i++) + { + args.text = String_UNSAFE_Substring(chars, i, 1); + charWidth = Drawer2D_TextWidth(&args); + + atlas->widths[i] = charWidth; + atlas->offsets[i] = width; + /* add 1 pixel of padding */ + width += charWidth + 1; + } + height = Drawer2D_TextHeight(&args); + + Context2D_Alloc(&ctx, width, height); + { + args.text = *prefix; + Context2D_DrawText(&ctx, &args, 0, 0); + + for (i = 0; i < chars->length; i++) + { + args.text = String_UNSAFE_Substring(chars, i, 1); + Context2D_DrawText(&ctx, &args, atlas->offsets[i], 0); + } + Context2D_MakeTexture(&atlas->tex, &ctx); + } + Context2D_Free(&ctx); + + atlas->uScale = 1.0f / (float)ctx.bmp.width; + atlas->tex.uv.u2 = atlas->offset * atlas->uScale; + atlas->tex.width = atlas->offset; +} + +void TextAtlas_Free(struct TextAtlas* atlas) { Gfx_DeleteTexture(&atlas->tex.ID); } + +void TextAtlas_Add(struct TextAtlas* atlas, int charI, struct VertexTextured** vertices) { + struct Texture part = atlas->tex; + int width = atlas->widths[charI]; + + part.x = atlas->curX; part.width = width; + part.uv.u1 = atlas->offsets[charI] * atlas->uScale; + part.uv.u2 = part.uv.u1 + width * atlas->uScale; + + atlas->curX += width; + Gfx_Make2DQuad(&part, PACKEDCOL_WHITE, vertices); +} + +void TextAtlas_AddInt(struct TextAtlas* atlas, int value, struct VertexTextured** vertices) { + char digits[STRING_INT_CHARS]; + int i, count; + + if (value < 0) { + TextAtlas_Add(atlas, 10, vertices); value = -value; /* - sign */ + } + count = String_MakeUInt32((cc_uint32)value, digits); + + for (i = count - 1; i >= 0; i--) + { + TextAtlas_Add(atlas, digits[i] - '0' , vertices); + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------Widget base-------------------------------------------------------* +*#########################################################################################################################*/ +void Widget_SetLocation(void* widget, cc_uint8 horAnchor, cc_uint8 verAnchor, int xOffset, int yOffset) { + struct Widget* w = (struct Widget*)widget; + w->horAnchor = horAnchor; w->verAnchor = verAnchor; + w->xOffset = Display_ScaleX(xOffset); + w->yOffset = Display_ScaleY(yOffset); + if (w->VTABLE) Widget_Layout(w); +} + +void Widget_CalcPosition(void* widget) { + struct Widget* w = (struct Widget*)widget; + int windowWidth, windowHeight; + +#ifdef CC_BUILD_DUALSCREEN + windowWidth = (w->flags & WIDGET_FLAG_MAINSCREEN) ? Window_Main.Width : Window_Alt.Width; + windowHeight = (w->flags & WIDGET_FLAG_MAINSCREEN) ? Window_Main.Height : Window_Alt.Height; +#else + windowWidth = Window_Main.Width; + windowHeight = Window_Main.Height; +#endif + + w->x = Gui_CalcPos(w->horAnchor, w->xOffset, w->width , windowWidth ); + w->y = Gui_CalcPos(w->verAnchor, w->yOffset, w->height, windowHeight); +} + +void Widget_Reset(void* widget) { + struct Widget* w = (struct Widget*)widget; + w->active = false; + w->flags = 0; + w->x = 0; w->y = 0; + w->width = 0; w->height = 0; + w->horAnchor = ANCHOR_MIN; + w->verAnchor = ANCHOR_MIN; + w->xOffset = 0; w->yOffset = 0; + w->MenuClick = NULL; + w->meta.ptr = NULL; +} + +int Widget_Contains(void* widget, int x, int y) { + struct Widget* w = (struct Widget*)widget; + return Gui_Contains(w->x, w->y, w->width, w->height, x, y); +} + +void Widget_SetDisabled(void* widget, int disabled) { + struct Widget* w = (struct Widget*)widget; + + if (disabled) { + w->flags |= WIDGET_FLAG_DISABLED; + } else { + w->flags &= ~WIDGET_FLAG_DISABLED; + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------Screen base-------------------------------------------------------* +*#########################################################################################################################*/ +void Screen_Render2Widgets(void* screen, float delta) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + int i, offset = 0; + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Gfx_BindDynamicVb(s->vb); + + for (i = 0; i < s->numWidgets; i++) + { + if (!widgets[i]) continue; + offset = Widget_Render2(widgets[i], offset); + } +} + +void Screen_UpdateVb(void* screen) { + struct Screen* s = (struct Screen*)screen; + Gfx_DeleteDynamicVb(&s->vb); + s->vb = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, s->maxVertices); +} + +struct VertexTextured* Screen_LockVb(void* screen) { + struct Screen* s = (struct Screen*)screen; + return (struct VertexTextured*)Gfx_LockDynamicVb(s->vb, + VERTEX_FORMAT_TEXTURED, s->maxVertices); +} + +int Screen_DoPointerDown(void* screen, int id, int x, int y) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + int i, count = s->numWidgets; + + /* iterate backwards (because last elements rendered are shown over others) */ + for (i = count - 1; i >= 0; i--) + { + struct Widget* w = widgets[i]; + if (!w || !Widget_Contains(w, x, y)) continue; + if (w->flags & WIDGET_FLAG_DISABLED) break; + + if (w->MenuClick) { + w->MenuClick(s, w); + } else { + Elem_HandlesPointerDown(w, id, x, y); + } + break; + } + return i; +} + +int Screen_CalcDefaultMaxVertices(void* screen) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + int i, count = 0; + + for (i = 0; i < s->numWidgets; i++) + { + if (!widgets[i]) continue; + count += widgets[i]->VTABLE->GetMaxVertices(widgets[i]); + } + return count; +} + + +void Screen_BuildMesh(void* screen) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + struct VertexTextured* data; + struct VertexTextured** ptr; + int i; + + data = Screen_LockVb(s); + ptr = &data; + + for (i = 0; i < s->numWidgets; i++) + { + if (!widgets[i]) continue; + Widget_BuildMesh(widgets[i], ptr); + } + Gfx_UnlockDynamicVb(s->vb); +} + +void Screen_Layout(void* screen) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + int i; + + for (i = 0; i < s->numWidgets; i++) + { + if (!widgets[i]) continue; + Widget_Layout(widgets[i]); + } +} + +void Screen_ContextLost(void* screen) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + int i; + Gfx_DeleteDynamicVb(&s->vb); + + for (i = 0; i < s->numWidgets; i++) + { + if (!widgets[i]) continue; + Elem_Free(widgets[i]); + } +} + +int Screen_InputDown(void* screen, int key) { return key < CCKEY_F1 || key > CCKEY_F24; } +void Screen_InputUp(void* screen, int key) { } +void Screen_PointerUp(void* s, int id, int x, int y) { } + +/*########################################################################################################################* +*------------------------------------------------------Input handling-----------------------------------------------------* +*#########################################################################################################################*/ +static void OnMouseWheel(void* obj, float delta) { + struct Screen* s; + int i; + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + s->dirty = true; + if (s->VTABLE->HandlesMouseScroll(s, delta)) return; + } +} + +static void OnPointerMove(void* obj, int idx) { + struct Screen* s; + int i, x = Pointers[idx].x, y = Pointers[idx].y; + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + s->dirty = true; + if (s->VTABLE->HandlesPointerMove(s, 1 << idx, x, y)) return; + } +} + +static void OnAxisUpdate(void* obj, int port, int axis, float x, float y) { + struct Screen* s; + int i; + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + if (!s->VTABLE->HandlesPadAxis) continue; + + s->dirty = true; + if (s->VTABLE->HandlesPadAxis(s, axis, x, y)) return; + } +} + + +/*########################################################################################################################* +*------------------------------------------------------Gui component------------------------------------------------------* +*#########################################################################################################################*/ +static void GuiPngProcess(struct Stream* stream, const cc_string* name) { + int heightDivisor = 2; /* only top half of gui png is used */ + Game_UpdateTexture(&Gui.GuiTex, stream, name, NULL, &heightDivisor); +} +static struct TextureEntry gui_entry = { "gui.png", GuiPngProcess }; + +static void GuiClassicPngProcess(struct Stream* stream, const cc_string* name) { + int heightDivisor = 2; /* only top half of gui png is used */ + Game_UpdateTexture(&Gui.GuiClassicTex, stream, name, NULL, &heightDivisor); +} +static struct TextureEntry guiClassic_entry = { "gui_classic.png", GuiClassicPngProcess }; + +static void IconsPngProcess(struct Stream* stream, const cc_string* name) { + int heightDivisor = 4; /* only top quarter of icons png is used */ + Game_UpdateTexture(&Gui.IconsTex, stream, name, NULL, &heightDivisor); +} +static struct TextureEntry icons_entry = { "icons.png", IconsPngProcess }; + +static void TouchPngProcess(struct Stream* stream, const cc_string* name) { + Game_UpdateTexture(&Gui.TouchTex, stream, name, NULL, NULL); +} +static struct TextureEntry touch_entry = { "touch.png", TouchPngProcess }; + +static void OnFontChanged(void* obj) { Gui_RefreshAll(); } + +static void OnKeyPress(void* obj, int cp) { + struct Screen* s; + int i; + char c; + if (!Convert_TryCodepointToCP437(cp, &c)) return; + + for (i = 0; i < Gui.ScreensCount; i++) + { + s = Gui_Screens[i]; + if (s->VTABLE->HandlesKeyPress(s, c)) return; + } +} + +static void OnTextChanged(void* obj, const cc_string* str) { + struct Screen* s; + int i; + + for (i = 0; i < Gui.ScreensCount; i++) + { + s = Gui_Screens[i]; + if (s->VTABLE->HandlesTextChanged(s, str)) return; + } +} + +static void OnContextLost(void* obj) { + LoseAllScreens(); + if (Gfx.ManagedTextures) return; + + Gfx_DeleteTexture(&Gui.GuiTex); + Gfx_DeleteTexture(&Gui.GuiClassicTex); + Gfx_DeleteTexture(&Gui.IconsTex); + Gfx_DeleteTexture(&Gui.TouchTex); +} + +static void OnInit(void) { + Gui.Screens = Gui_Screens; /* for plugins */ + TextureEntry_Register(&gui_entry); + TextureEntry_Register(&guiClassic_entry); + TextureEntry_Register(&icons_entry); + TextureEntry_Register(&touch_entry); + + Event_Register_(&InputEvents.Wheel, NULL, OnMouseWheel); + Event_Register_(&PointerEvents.Moved, NULL, OnPointerMove); + Event_Register_(&ControllerEvents.AxisUpdate, NULL, OnAxisUpdate); + +#ifdef CC_BUILD_DUALSCREEN + struct Context2D ctx; + Context2D_Alloc(&ctx, 32, 32); + Gradient_Noise(&ctx, BitmapColor_RGB(0x40, 0x30, 0x20), 6, 0, 0, ctx.width, ctx.height); + Context2D_MakeTexture(&touchBgTex, &ctx); + Context2D_Free(&ctx); + + // Tile the texture to fill the entire screen + int tilesX = Math_CeilDiv(Window_Alt.Width, ctx.width); + int tilesY = Math_CeilDiv(Window_Alt.Height, ctx.height); + touchBgTex.width *= tilesX; touchBgTex.height *= tilesY; + touchBgTex.uv.u2 *= tilesX; touchBgTex.uv.v2 *= tilesY; +#endif + + Event_Register_(&ChatEvents.FontChanged, NULL, OnFontChanged); + Event_Register_(&GfxEvents.ContextLost, NULL, OnContextLost); + Event_Register_(&GfxEvents.ContextRecreated, NULL, OnContextRecreated); + Event_Register_(&InputEvents.Press, NULL, OnKeyPress); + Event_Register_(&WindowEvents.Resized, NULL, OnResize); + Event_Register_(&InputEvents.TextChanged, NULL, OnTextChanged); + + LoadOptions(); + Gui_ShowDefault(); +} + +static void OnReset(void) { + /* TODO:Should we reset all screens here.. ? */ +} + +static void OnFree(void) { + while (Gui.ScreensCount) Gui_Remove(Gui_Screens[0]); + + OnContextLost(NULL); + OnReset(); +} + +struct IGameComponent Gui_Component = { + OnInit, /* Init */ + OnFree, /* Free */ + OnReset, /* Reset */ + NULL, /* OnNewMap */ + NULL, /* OnNewMapLoaded */ +}; diff --git a/src/Gui.h b/src/Gui.h new file mode 100644 index 0000000..16e0c73 --- /dev/null +++ b/src/Gui.h @@ -0,0 +1,306 @@ +#ifndef CC_GUI_H +#define CC_GUI_H +#include "Core.h" +/* Describes and manages 2D GUI elements on screen. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +enum GuiAnchor { + ANCHOR_MIN, /* = offset */ + ANCHOR_CENTRE, /* = (axis/2) - (size/2) - offset; */ + ANCHOR_MAX, /* = axis - size - offset */ + ANCHOR_CENTRE_MIN, /* = (axis/2) + offset */ + ANCHOR_CENTRE_MAX /* = (axis/2) - size - offset */ +}; + +struct IGameComponent; +struct VertexTextured; +struct FontDesc; +struct Widget; +extern struct IGameComponent Gui_Component; + +CC_VAR extern struct _GuiData { + /* The list of screens currently shown. */ + struct Screen** Screens; + /* The number of screens currently shown. */ + int ScreensCount; + /* Whether vanilla Minecraft Classic gui texture is used. */ + cc_bool ClassicTexture; + /* Whether tab list is laid out like vanilla Minecraft Classic. */ + cc_bool ClassicTabList; + /* Whether menus are laid out like vanilla Minecraft Classic. */ + cc_bool ClassicMenu; + /* Whether classic-style chat screen is used */ + cc_bool ClassicChat; + /* Maximum number of visible chatlines on screen. Can be 0. */ + int Chatlines; + /* Whether clicking on a chatline inserts it into chat input. */ + cc_bool ClickableChat; + /* Whether pressing tab in chat input attempts to autocomplete player names. */ + cc_bool TabAutocomplete; + /* Whether FPS counter (and other info) is shown in top left. */ + cc_bool ShowFPS; + /* Whether classic-style inventory is used */ + cc_bool ClassicInventory; + float RawHotbarScale, RawChatScale, RawInventoryScale, RawCrosshairScale; + GfxResourceID GuiTex, GuiClassicTex, IconsTex, TouchTex; + int DefaultLines; + int _unused; + float RawTouchScale; + /* The highest priority screen that has grabbed input. */ + struct Screen* InputGrab; + /* Whether chat automatically scales based on window size. */ + cc_bool AutoScaleChat; + /* Whether the touch UI is currently being displayed */ + cc_bool TouchUI; +} Gui; + +#ifdef CC_BUILD_TOUCH +#define Gui_TouchUI Gui.TouchUI +#else +#define Gui_TouchUI false +#endif + +float Gui_Scale(float value); +float Gui_GetHotbarScale(void); +float Gui_GetInventoryScale(void); +float Gui_GetChatScale(void); +float Gui_GetCrosshairScale(void); + +CC_NOINLINE void Gui_MakeTitleFont(struct FontDesc* font); +CC_NOINLINE void Gui_MakeBodyFont(struct FontDesc* font); + +/* Functions for a Screen instance. */ +struct ScreenVTABLE { + /* Initialises persistent state. */ + void (*Init)(void* elem); + /* Updates this screen, called every frame just before Render(). */ + void (*Update)(void* elem, float delta); + /* Frees/releases persistent state. */ + void (*Free)(void* elem); + /* Draws this screen and its widgets on screen. */ + void (*Render)(void* elem, float delta); + /* Builds the vertex mesh for all the widgets in the screen. */ + void (*BuildMesh)(void* elem); + /* Returns non-zero if an input press is handled. */ + int (*HandlesInputDown)(void* elem, int key); + /* Called when an input key or button is released */ + void (*OnInputUp)(void* elem, int key); + /* Returns non-zero if a key character press is handled. */ + int (*HandlesKeyPress)(void* elem, char keyChar); + /* Returns non-zero if on-screen keyboard text changed is handled. */ + int (*HandlesTextChanged)(void* elem, const cc_string* str); + /* Returns non-zero if a pointer press is handled. */ + int (*HandlesPointerDown)(void* elem, int id, int x, int y); + /* Called when a pointer is released. */ + void (*OnPointerUp)(void* elem, int id, int x, int y); + /* Returns non-zero if a pointer movement is handled. */ + int (*HandlesPointerMove)(void* elem, int id, int x, int y); + /* Returns non-zero if a mouse wheel scroll is handled. */ + int (*HandlesMouseScroll)(void* elem, float delta); + /* Positions widgets on screen. Typically called on window resize. */ + void (*Layout)(void* elem); + /* Destroys graphics resources. (textures, vertex buffers, etc) */ + void (*ContextLost)(void* elem); + /* Allocates graphics resources. (textures, vertex buffers, etc) */ + void (*ContextRecreated)(void* elem); + /* Returns non-zero if a pad axis update is handled. */ + int (*HandlesPadAxis)(void* elem, int axis, float x, float y); +}; +#define Screen_Body const struct ScreenVTABLE* VTABLE; \ + cc_bool grabsInput; /* Whether this screen grabs input. Causes the cursor to become visible. */ \ + cc_bool blocksWorld; /* Whether this screen completely and opaquely covers the game world behind it. */ \ + cc_bool closable; /* Whether this screen is automatically closed when pressing Escape */ \ + cc_bool dirty; /* Whether this screens needs to have its mesh rebuilt. */ \ + int maxVertices; GfxResourceID vb; /* Vertex buffer storing the contents of the screen */ \ + struct Widget** widgets; int numWidgets; /* The widgets/individual elements in the screen */ \ + int selectedI, maxWidgets; + +/* Represents a container of widgets and other 2D elements. May cover entire window. */ +struct Screen { Screen_Body }; +/* Calls Widget_Render2 on each widget in the screen. */ +void Screen_Render2Widgets(void* screen, float delta); +void Screen_UpdateVb(void* screen); +struct VertexTextured* Screen_LockVb(void* screen); +int Screen_DoPointerDown(void* screen, int id, int x, int y); +int Screen_CalcDefaultMaxVertices(void* screen); + +/* Default mesh building implementation for a screen */ +/* (Locks vb, calls Widget_BuildMesh on each widget, then unlocks vb) */ +void Screen_BuildMesh(void* screen); +/* Default layout implementation for a screen */ +/* (Calls Widget_Layout on each widget) */ +void Screen_Layout(void* screen); +/* Default context lost implementation for a screen */ +/* (Deletes vb, then calls Elem_Free on each widget) */ +void Screen_ContextLost(void* screen); +/* Default input down implementation for a screen */ +/* (returns true if key is NOT a function key) */ +int Screen_InputDown(void* screen, int key); +/* Default input up implementation for a screen */ +/* (does nothing) */ +void Screen_InputUp(void* screen, int key); +/* Default pointer release implementation for a screen */ +/* (does nothing) */ +void Screen_PointerUp(void* s, int id, int x, int y); + + +typedef void (*Widget_LeftClick)(void* screen, void* widget); +union WidgetMeta { int val; void* ptr; }; + +struct WidgetVTABLE { + /* Draws this widget on-screen. */ + void (*Render)(void* elem, float delta); + /* Destroys allocated graphics resources. */ + void (*Free)(void* elem); + /* Positions this widget on-screen. */ + void (*Reposition)(void* elem); + /* Returns non-zero if an input press is handled. */ + int (*HandlesKeyDown)(void* elem, int key); + /* Called when an input key or button is released. */ + void (*OnInputUp)(void* elem, int key); + /* Returns non-zero if a mouse wheel scroll is handled. */ + int (*HandlesMouseScroll)(void* elem, float delta); + /* Returns non-zero if a pointer press is handled. */ + int (*HandlesPointerDown)(void* elem, int id, int x, int y); + /* Called when a pointer is released. */ + void (*OnPointerUp)(void* elem, int id, int x, int y); + /* Returns non-zero if a pointer movement is handled. */ + int (*HandlesPointerMove)(void* elem, int id, int x, int y); + /* Builds the mesh of vertices for this widget. */ + void (*BuildMesh)(void* elem, struct VertexTextured** vertices); + /* Draws this widget on-screen. */ + int (*Render2)(void* elem, int offset); + /* Returns the maximum number of vertices this widget may use */ + int (*GetMaxVertices)(void* elem); + /* Returns non-zero if a pad axis update is handled. */ + int (*HandlesPadAxis)(void* elem, int axis, float x, float y); +}; + +#define Widget_Body const struct WidgetVTABLE* VTABLE; \ + int x, y, width, height; /* Top left corner, and dimensions, of this widget */ \ + cc_bool active; /* Whether this widget is currently being moused over */ \ + cc_uint8 flags; /* Flags controlling the widget's interactability */ \ + cc_uint8 horAnchor, verAnchor; /* The reference point for when this widget is resized */ \ + int xOffset, yOffset; /* Offset from the reference point */ \ + Widget_LeftClick MenuClick; \ + union WidgetMeta meta; + +/* Whether a widget is prevented from being interacted with */ +#define WIDGET_FLAG_DISABLED 0x01 +/* Whether a widget can be selected via up/down */ +#define WIDGET_FLAG_SELECTABLE 0x02 +/* Whether for dual screen builds, this widget still appears on */ +/* the main game screen instead of the dedicated UI screen */ +#define WIDGET_FLAG_MAINSCREEN 0x04 +#ifdef CC_BUILD_DUALSCREEN + #define Window_UI Window_Alt +#else + #define Window_UI Window_Main +#endif + +/* Represents an individual 2D gui component. */ +struct Widget { Widget_Body }; +void Widget_SetLocation(void* widget, cc_uint8 horAnchor, cc_uint8 verAnchor, int xOffset, int yOffset); +/* Calculates where this widget should be on-screen based on its attributes. */ +/* These attributes are width/height, horAnchor/verAnchor, xOffset/yOffset */ +void Widget_CalcPosition(void* widget); +/* Resets Widget struct fields to 0/NULL (except VTABLE) */ +void Widget_Reset(void* widget); +/* Returns non-zero if the given point is located within the bounds of the widget. */ +int Widget_Contains(void* widget, int x, int y); +/* Sets whether the widget is prevented from being interacted with */ +void Widget_SetDisabled(void* widget, int disabled); + + +/* Higher priority handles input first and draws on top */ +/* NOTE: Values are 5 apart to allow plugins to insert custom screens */ +enum GuiPriority { + GUI_PRIORITY_DISCONNECT = 60, + GUI_PRIORITY_OLDLOADING = 55, + GUI_PRIORITY_MENUINPUT = 57, + GUI_PRIORITY_MENU = 50, + GUI_PRIORITY_TOUCHMORE = 45, + GUI_PRIORITY_URLWARNING = 40, + GUI_PRIORITY_TEXPACK = 35, + GUI_PRIORITY_TEXIDS = 30, + GUI_PRIORITY_TOUCH = 25, + GUI_PRIORITY_INVENTORY = 20, + GUI_PRIORITY_TABLIST = 17, + GUI_PRIORITY_CHAT = 15, + GUI_PRIORITY_HUD = 10, + GUI_PRIORITY_LOADING = 5 +}; + +#define GUI_MAX_SCREENS 10 +extern struct Screen* Gui_Screens[GUI_MAX_SCREENS]; + +/* Calculates position of an element on a particular axis */ +/* For example, to calculate X position of a text widget on screen */ +int Gui_CalcPos(cc_uint8 anchor, int offset, int size, int axisLen); +/* Returns non-zero if the given rectangle contains the given point. */ +int Gui_Contains(int recX, int recY, int width, int height, int x, int y); +/* Returns non-zero if one or more pointers lie within the given rectangle. */ +int Gui_ContainsPointers(int x, int y, int width, int height); +/* Shows HUD and Status screens. */ +void Gui_ShowDefault(void); +#ifdef CC_BUILD_TOUCH +/* Sets whether touch UI should be displayed or not */ +void Gui_SetTouchUI(cc_bool enabled); +#endif + +/* (internal) Removes the screen from the screens list. */ +/* NOTE: This does NOT perform the usual 'screens changed' behaviour. */ +void Gui_RemoveCore(struct Screen* s); +/* Removes the screen from the screens list. */ +CC_API void Gui_Remove(struct Screen* screen); +/* Inserts a screen into the screen lists with the given priority. */ +/* NOTE: If there is an existing screen with the same priority, it is removed. */ +CC_API void Gui_Add(struct Screen* screen, int priority); + +/* Returns highest priority screen that has grabbed input. */ +CC_API struct Screen* Gui_GetInputGrab(void); +/* Returns highest priority screen that blocks world rendering. */ +struct Screen* Gui_GetBlocksWorld(void); +/* Returns highest priority screen that is closable. */ +struct Screen* Gui_GetClosable(void); +/* Returns screen with the given priority */ +CC_API struct Screen* Gui_GetScreen(int priority); +void Gui_UpdateInputGrab(void); +void Gui_ShowPauseMenu(void); + +void Gui_LayoutAll(void); +void Gui_RefreshAll(void); +void Gui_Refresh(struct Screen* s); +void Gui_RenderGui(float delta); + +#define TEXTATLAS_MAX_WIDTHS 16 +struct TextAtlas { + struct Texture tex; + int offset, curX; + float uScale; + short widths[TEXTATLAS_MAX_WIDTHS]; + short offsets[TEXTATLAS_MAX_WIDTHS]; +}; +void TextAtlas_Make(struct TextAtlas* atlas, const cc_string* chars, struct FontDesc* font, const cc_string* prefix); +void TextAtlas_Free(struct TextAtlas* atlas); +void TextAtlas_Add(struct TextAtlas* atlas, int charI, struct VertexTextured** vertices); +void TextAtlas_AddInt(struct TextAtlas* atlas, int value, struct VertexTextured** vertices); + +#define Elem_Render(elem, delta) (elem)->VTABLE->Render(elem, delta) +#define Elem_Free(elem) (elem)->VTABLE->Free(elem) +#define Elem_HandlesKeyPress(elem, key) (elem)->VTABLE->HandlesKeyPress(elem, key) +#define Elem_HandlesKeyDown(elem, key) (elem)->VTABLE->HandlesKeyDown(elem, key) +#define Elem_OnInputUp(elem, key) (elem)->VTABLE->OnInputUp(elem, key) + +#define Elem_HandlesMouseScroll(elem, delta) (elem)->VTABLE->HandlesMouseScroll(elem, delta) +#define Elem_HandlesPointerDown(elem, id, x, y) (elem)->VTABLE->HandlesPointerDown(elem, id, x, y) +#define Elem_OnPointerUp(elem, id, x, y) (elem)->VTABLE->OnPointerUp(elem, id, x, y) +#define Elem_HandlesPointerMove(elem, id, x, y) (elem)->VTABLE->HandlesPointerMove(elem, id, x, y) + +#define Elem_HandlesPadAxis(elem, axis, x, y) (elem)->VTABLE->HandlesPadAxis(elem, axis, x, y) + +#define Widget_BuildMesh(widget, vertices) (widget)->VTABLE->BuildMesh(widget, vertices) +#define Widget_Render2(widget, offset) (widget)->VTABLE->Render2(widget, offset) +#define Widget_Layout(widget) (widget)->VTABLE->Reposition(widget) +#endif diff --git a/src/HeldBlockRenderer.c b/src/HeldBlockRenderer.c new file mode 100644 index 0000000..93511c3 --- /dev/null +++ b/src/HeldBlockRenderer.c @@ -0,0 +1,267 @@ +#include "HeldBlockRenderer.h" +#include "Block.h" +#include "Game.h" +#include "Inventory.h" +#include "Graphics.h" +#include "Camera.h" +#include "ExtMath.h" +#include "Event.h" +#include "Entity.h" +#include "Model.h" +#include "Options.h" + +cc_bool HeldBlockRenderer_Show; +static BlockID held_block; +static struct Entity held_entity; +static struct Matrix held_blockProj; + +static cc_bool held_animating, held_breaking, held_swinging; +static float held_swingY; +static float held_time, held_period = 0.25f; +static BlockID held_lastBlock; + +/* Since not using Entity_SetModel, which normally automatically does this */ +static void SetHeldModel(struct Model* model) { +#ifdef CC_BUILD_CONSOLE + static int maxVertices; + if (model->maxVertices <= maxVertices) return; + + maxVertices = model->maxVertices; + Gfx_DeleteDynamicVb(&held_entity.ModelVB); +#endif +} + +static void HeldBlockRenderer_RenderModel(void) { + struct Model* model; + + Gfx_SetFaceCulling(true); + Gfx_SetDepthTest(false); + /* Gfx_SetDepthWrite(false); */ + /* TODO: Need to properly reallocate per model VB here */ + + if (Blocks.Draw[held_block] == DRAW_GAS) { + model = Entities.CurPlayer->Base.Model; + SetHeldModel(model); + Vec3_Set(held_entity.ModelScale, 1.0f,1.0f,1.0f); + + Model_RenderArm(model, &held_entity); + Gfx_SetAlphaTest(false); + } else { + model = Models.Block; + SetHeldModel(model); + Vec3_Set(held_entity.ModelScale, 0.4f,0.4f,0.4f); + + Gfx_SetupAlphaState(Blocks.Draw[held_block]); + Model_Render(model, &held_entity); + Gfx_RestoreAlphaState(Blocks.Draw[held_block]); + } + + Gfx_SetDepthTest(true); + /* Gfx_SetDepthWrite(true); */ + Gfx_SetFaceCulling(false); +} + +static void SetMatrix(void) { + struct Entity* p = &Entities.CurPlayer->Base; + struct Matrix lookAt; + Vec3 eye = { 0,0,0 }; eye.y = Entity_GetEyeHeight(p); + + Matrix_Translate(&lookAt, -eye.x, -eye.y, -eye.z); + Matrix_Mul(&Gfx.View, &lookAt, &Camera.TiltM); +} + +static void ResetHeldState(void) { + /* Based off details from http://pastebin.com/KFV0HkmD (Thanks goodlyay!) */ + struct Entity* p = &Entities.CurPlayer->Base; + Vec3 eye = { 0,0,0 }; eye.y = Entity_GetEyeHeight(p); + held_entity.Position = eye; + + held_entity.Position.x -= Camera.BobbingHor; + held_entity.Position.y -= Camera.BobbingVer; + held_entity.Position.z -= Camera.BobbingHor; + + held_entity.Yaw = -45.0f; held_entity.RotY = -45.0f; + held_entity.Pitch = 0.0f; held_entity.RotX = 0.0f; + held_entity.ModelBlock = held_block; + + held_entity.SkinType = p->SkinType; + held_entity.TextureId = p->TextureId; + held_entity.MobTextureId = p->MobTextureId; + held_entity.uScale = p->uScale; + held_entity.vScale = p->vScale; +} + +static void SetBaseOffset(void) { + cc_bool sprite = Blocks.Draw[held_block] == DRAW_SPRITE; + Vec3 normalOffset = { 0.56f, -0.72f, -0.72f }; + Vec3 spriteOffset = { 0.46f, -0.52f, -0.72f }; + Vec3 offset = sprite ? spriteOffset : normalOffset; + + Vec3_AddBy(&held_entity.Position, &offset); + if (!sprite && Blocks.Draw[held_block] != DRAW_GAS) { + float height = Blocks.MaxBB[held_block].y - Blocks.MinBB[held_block].y; + held_entity.Position.y += 0.2f * (1.0f - height); + } +} + +static void OnProjectionChanged(void* obj) { + float fov = 70.0f * MATH_DEG2RAD; + float aspectRatio = (float)Game.Width / (float)Game.Height; + Gfx_CalcPerspectiveMatrix(&held_blockProj, fov, aspectRatio, (float)Game_ViewDistance); +} + +/* Based off incredible gifs from (Thanks goodlyay!) + https://dl.dropboxusercontent.com/s/iuazpmpnr89zdgb/slowBreakTranslate.gif + https://dl.dropboxusercontent.com/s/z7z8bset914s0ij/slowBreakRotate1.gif + https://dl.dropboxusercontent.com/s/pdq79gkzntquld1/slowBreakRotate2.gif + https://dl.dropboxusercontent.com/s/w1ego7cy7e5nrk1/slowBreakFull.gif + + https://github.com/UnknownShadow200/ClassicalSharp/wiki/Dig-animation-details +*/ +static void HeldBlockRenderer_DigAnimation(void) { + float sinHalfCircle, sinHalfCircleWeird; + float t, sqrtLerpPI; + + t = held_time / held_period; + sinHalfCircle = Math_SinF(t * MATH_PI); + sqrtLerpPI = Math_SqrtF(t) * MATH_PI; + + held_entity.Position.x -= Math_SinF(sqrtLerpPI) * 0.4f; + held_entity.Position.y += Math_SinF(sqrtLerpPI * 2) * 0.2f; + held_entity.Position.z -= sinHalfCircle * 0.2f; + + sinHalfCircleWeird = Math_SinF(t * t * MATH_PI); + held_entity.RotY -= Math_SinF(sqrtLerpPI) * 80.0f; + held_entity.Yaw -= Math_SinF(sqrtLerpPI) * 80.0f; + held_entity.RotX += sinHalfCircleWeird * 20.0f; +} + +static void HeldBlockRenderer_ResetAnim(cc_bool setLastHeld, float period) { + held_time = 0.0f; held_swingY = 0.0f; + held_animating = false; held_swinging = false; + held_period = period; + if (setLastHeld) { held_lastBlock = Inventory_SelectedBlock; } +} + +static PackedCol HeldBlockRenderer_GetCol(struct Entity* entity) { + struct Entity* player; + PackedCol col; + float adjPitch, t, scale; + + player = &Entities.CurPlayer->Base; + col = player->VTABLE->GetCol(player); + + /* Adjust pitch so angle when looking straight down is 0. */ + adjPitch = player->Pitch - 90.0f; + if (adjPitch < 0.0f) adjPitch += 360.0f; + + /* Adjust color so held block is brighter when looking straight up */ + t = Math_AbsF(adjPitch - 180.0f) / 180.0f; + scale = Math_Lerp(0.9f, 0.7f, t); + return PackedCol_Scale(col, scale); +} + +void HeldBlockRenderer_ClickAnim(cc_bool digging) { + /* TODO: timing still not quite right, rotate2 still not quite right */ + HeldBlockRenderer_ResetAnim(true, digging ? 0.35 : 0.25); + held_swinging = false; + held_breaking = digging; + held_animating = true; + /* Start place animation at bottom of cycle */ + if (!digging) held_time = held_period / 2; +} + +static void DoSwitchBlockAnim(void* obj) { + if (held_swinging) { + /* Like graph -sin(x) : x=0.5 and x=2.5 have same y values, + but increasing x causes y to change in opposite directions */ + if (held_time > held_period * 0.5f) { + held_time = held_period - held_time; + } + } else { + if (held_block == Inventory_SelectedBlock) return; + HeldBlockRenderer_ResetAnim(false, 0.25); + held_animating = true; + held_swinging = true; + } +} + +static void OnBlockChanged(void* obj, IVec3 coords, BlockID old, BlockID now) { + if (now == BLOCK_AIR) return; + HeldBlockRenderer_ClickAnim(false); +} + +static void DoAnimation(float delta, float lastSwingY) { + float t; + if (!held_animating) return; + + if (held_swinging || !held_breaking) { + t = held_time / held_period; + held_swingY = -0.4f * Math_SinF(t * MATH_PI); + held_entity.Position.y += held_swingY; + + if (held_swinging) { + /* i.e. the block has gone to bottom of screen and is now returning back up. + At this point we switch over to the new held block. */ + if (held_swingY > lastSwingY) held_lastBlock = held_block; + held_block = held_lastBlock; + held_entity.ModelBlock = held_block; + } + } else { + HeldBlockRenderer_DigAnimation(); + } + + held_time += delta; + if (held_time > held_period) { + HeldBlockRenderer_ResetAnim(true, 0.25f); + } +} + +void HeldBlockRenderer_Render(float delta) { + float lastSwingY; + struct Matrix view; + if (!HeldBlockRenderer_Show) return; + + lastSwingY = held_swingY; + held_swingY = 0.0f; + held_block = Inventory_SelectedBlock; + view = Gfx.View; + + Gfx_LoadMatrix(MATRIX_PROJECTION, &held_blockProj); + SetMatrix(); + + ResetHeldState(); + DoAnimation(delta, lastSwingY); + SetBaseOffset(); + if (!Camera.Active->isThirdPerson) HeldBlockRenderer_RenderModel(); + + Gfx.View = view; + Gfx_LoadMatrix(MATRIX_PROJECTION, &Gfx.Projection); +} + + +static void OnContextLost(void* obj) { + Gfx_DeleteDynamicVb(&held_entity.ModelVB); +} + +static const struct EntityVTABLE heldEntity_VTABLE = { + NULL, NULL, NULL, HeldBlockRenderer_GetCol, + NULL, NULL +}; +static void OnInit(void) { + Entity_Init(&held_entity); + held_entity.VTABLE = &heldEntity_VTABLE; + held_entity.NoShade = true; + + HeldBlockRenderer_Show = Options_GetBool(OPT_SHOW_BLOCK_IN_HAND, true); + held_lastBlock = Inventory_SelectedBlock; + + Event_Register_(&GfxEvents.ProjectionChanged, NULL, OnProjectionChanged); + Event_Register_(&UserEvents.HeldBlockChanged, NULL, DoSwitchBlockAnim); + Event_Register_(&UserEvents.BlockChanged, NULL, OnBlockChanged); + Event_Register_(&GfxEvents.ContextLost, NULL, OnContextLost); +} + +struct IGameComponent HeldBlockRenderer_Component = { + OnInit /* Init */ +}; diff --git a/src/HeldBlockRenderer.h b/src/HeldBlockRenderer.h new file mode 100644 index 0000000..9d153a5 --- /dev/null +++ b/src/HeldBlockRenderer.h @@ -0,0 +1,15 @@ +#ifndef CC_HELDBLOCKRENDERER_H +#define CC_HELDBLOCKRENDERER_H +#include "Core.h" +/* +Renders the held block/arm at bottom right of game +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent HeldBlockRenderer_Component; +/* Whether held block/arm should be shown at all. */ +extern cc_bool HeldBlockRenderer_Show; + +void HeldBlockRenderer_ClickAnim(cc_bool digging); +void HeldBlockRenderer_Render(float delta); +#endif diff --git a/src/Http.h b/src/Http.h new file mode 100644 index 0000000..19a7ad5 --- /dev/null +++ b/src/Http.h @@ -0,0 +1,90 @@ +#ifndef CC_HTTP_H +#define CC_HTTP_H +#include "Constants.h" +#include "Core.h" +/* +Aysnchronously performs http GET, HEAD, and POST requests + Typically this is used to download skins, texture packs, etc +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +struct ScheduledTask; +struct StringsBuffer; + +#define URL_MAX_SIZE (STRING_SIZE * 2) +#define HTTP_FLAG_PRIORITY 0x01 +#define HTTP_FLAG_NOCACHE 0x02 + +extern struct IGameComponent Http_Component; + +enum HttpRequestType { REQUEST_TYPE_GET, REQUEST_TYPE_HEAD, REQUEST_TYPE_POST }; +enum HttpProgress { + HTTP_PROGRESS_NOT_WORKING_ON = -3, + HTTP_PROGRESS_MAKING_REQUEST = -2, + HTTP_PROGRESS_FETCHING_DATA = -1 +}; + +struct HttpRequest { + char url[URL_MAX_SIZE]; /* URL data is downloaded from/uploaded to. */ + int id; /* Unique identifier for this request. */ + volatile int progress; /* Progress with downloading this request */ + TimeMS timeDownloaded; /* Time response contents were completely downloaded. */ + int statusCode; /* HTTP status code returned in the response. */ + cc_uint32 contentLength; /* HTTP content length returned in the response. */ + + cc_result result; /* 0 on success, otherwise platform-specific error. */ + cc_uint8* data; /* Contents of the response. (i.e. result data) */ + cc_uint32 size; /* Size of the contents. */ + cc_uint32 _capacity; /* (private) Maximum size of data buffer */ + void* meta; /* Pointer to backend specific data */ + char* error; /* Pointer to dynamically allocated error message */ + + char lastModified[STRING_SIZE]; /* Time item cached at (if at all) */ + char etag[STRING_SIZE]; /* ETag of cached item (if any) */ + cc_uint8 requestType; /* See the various REQUEST_TYPE_ */ + cc_bool success; /* Whether Result is 0, status is 200, and data is not NULL */ + struct StringsBuffer* cookies; /* Cookie list sent in requests. May be modified by the response. */ +}; + +/* Frees all dynamically allocated data from a HTTP request */ +void HttpRequest_Free(struct HttpRequest* request); + +/* Aschronously performs a http GET request to download a skin. */ +/* If url is a skin, downloads from there. (if not, downloads from SKIN_SERVER/[skinName].png) */ +int Http_AsyncGetSkin(const cc_string* skinName, cc_uint8 flags); +/* Asynchronously performs a http GET request. (e.g. to download data) */ +int Http_AsyncGetData(const cc_string* url, cc_uint8 flags); +/* Asynchronously performs a http HEAD request. (e.g. to get Content-Length header) */ +int Http_AsyncGetHeaders(const cc_string* url, cc_uint8 flags); +/* Asynchronously performs a http POST request. (e.g. to submit data) */ +/* NOTE: You don't have to persist data, a copy is made of it. */ +int Http_AsyncPostData(const cc_string* url, cc_uint8 flags, const void* data, cc_uint32 size, struct StringsBuffer* cookies); +/* Asynchronously performs a http GET request. (e.g. to download data) */ +/* Also sets the If-Modified-Since and If-None-Match headers. (if not NULL) */ +int Http_AsyncGetDataEx(const cc_string* url, cc_uint8 flags, const cc_string* lastModified, const cc_string* etag, struct StringsBuffer* cookies); +/* Attempts to remove given request from pending and finished request lists. */ +/* NOTE: Won't cancel the request if it is currently in progress. */ +void Http_TryCancel(int reqID); + +/* Encodes data using % or URL encoding. */ +void Http_UrlEncode(cc_string* dst, const cc_uint8* data, int len); +/* Converts characters to UTF8, then calls Http_UrlEncode on them. */ +void Http_UrlEncodeUtf8(cc_string* dst, const cc_string* src); +/* Outputs more detailed information about errors with http requests. */ +cc_bool Http_DescribeError(cc_result res, cc_string* dst); + +/* Attempts to retrieve a fully completed request. */ +/* NOTE: You MUST check Success for whether it completed successfully. */ +/* (Data may still be non NULL even on error, e.g. on a http 404 error) */ +cc_bool Http_GetResult(int reqID, struct HttpRequest* item); +/* Retrieves information about the request currently being processed. */ +cc_bool Http_GetCurrent(int* reqID, int* progress); +/* Retrieves information about the download progress of the given request. */ +/* NOTE: This may return HTTP_PROGRESS_NOT_WORKING_ON if download has finished. */ +/* As such, this method should always be paired with a call to Http_GetResult. */ +int Http_CheckProgress(int reqID); +/* Clears the list of pending requests. */ +void Http_ClearPending(void); + +void Http_LogError(const char* action, const struct HttpRequest* item); +#endif diff --git a/src/Http_Web.c b/src/Http_Web.c new file mode 100644 index 0000000..17da0a1 --- /dev/null +++ b/src/Http_Web.c @@ -0,0 +1,148 @@ +#include "Core.h" +#ifdef CC_BUILD_WEB +#include "_HttpBase.h" +#include +#include "Errors.h" +extern int interop_DownloadAsync(const char* url, int method, int reqID); +extern int interop_IsHttpsOnly(void); +static struct RequestList workingReqs, queuedReqs; +static cc_uint64 startTime; + + +/*########################################################################################################################* +*----------------------------------------------------Http public api------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Http_GetResult(int reqID, struct HttpRequest* item) { + int i = RequestList_Find(&processedReqs, reqID); + + if (i >= 0) *item = processedReqs.entries[i]; + if (i >= 0) RequestList_RemoveAt(&processedReqs, i); + return i >= 0; +} + +cc_bool Http_GetCurrent(int* reqID, int* progress) { + /* TODO: Stubbed as this isn't required at the moment */ + *progress = 0; + return 0; +} + +int Http_CheckProgress(int reqID) { + int idx = RequestList_Find(&workingReqs, reqID); + if (idx == -1) return HTTP_PROGRESS_NOT_WORKING_ON; + + return workingReqs.entries[idx].progress; +} + +void Http_ClearPending(void) { + RequestList_Free(&queuedReqs); + RequestList_Free(&workingReqs); +} + +void Http_TryCancel(int reqID) { + RequestList_TryFree(&queuedReqs, reqID); + RequestList_TryFree(&workingReqs, reqID); + RequestList_TryFree(&processedReqs, reqID); +} + + +/*########################################################################################################################* +*----------------------------------------------------Emscripten backend---------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +#define HTTP_MAX_CONCURRENCY 6 +static void Http_StartNextDownload(void) { + char urlBuffer[URL_MAX_SIZE]; cc_string url; + char urlStr[NATIVE_STR_LEN]; + struct HttpRequest* req; + cc_result res; + + /* Avoid making too many requests at once */ + if (workingReqs.count >= HTTP_MAX_CONCURRENCY) return; + if (!queuedReqs.count) return; + String_InitArray(url, urlBuffer); + + req = &queuedReqs.entries[0]; + Http_GetUrl(req, &url); + Platform_Log1("Fetching %s", &url); + + String_EncodeUtf8(urlStr, &url); + res = interop_DownloadAsync(urlStr, req->requestType, req->id); + + if (res) { + /* interop error code -> ClassiCube error code */ + if (res == 1) res = ERR_INVALID_DATA_URL; + req->result = res; + + /* Invalid URL so move onto next request */ + Http_FinishRequest(req); + RequestList_RemoveAt(&queuedReqs, 0); + Http_StartNextDownload(); + } else { + RequestList_Append(&workingReqs, req, false); + RequestList_RemoveAt(&queuedReqs, 0); + } +} + +EMSCRIPTEN_KEEPALIVE void Http_OnUpdateProgress(int reqID, int read, int total) { + int idx = RequestList_Find(&workingReqs, reqID); + if (idx == -1 || !total) return; + + workingReqs.entries[idx].progress = (int)(100.0f * read / total); +} + +EMSCRIPTEN_KEEPALIVE void Http_OnFinishedAsync(int reqID, void* data, int len, int status) { + struct HttpRequest* req; + int idx = RequestList_Find(&workingReqs, reqID); + + if (idx == -1) { + /* Shouldn't ever happen, but log a warning anyways */ + Mem_Free(data); + Platform_Log1("Ignoring invalid request (%i)", &reqID); + } else { + req = &workingReqs.entries[idx]; + req->data = data; + req->size = len; + req->statusCode = status; + req->contentLength = len; + + /* Usually this happens when denied by CORS */ + if (!status && !data) req->result = ERR_DOWNLOAD_INVALID; + + if (req->data) Platform_Log1("HTTP returned data: %i bytes", &req->size); + Http_FinishRequest(req); + RequestList_RemoveAt(&workingReqs, idx); + } + Http_StartNextDownload(); +} + +/* Adds a req to the list of pending requests, waking up worker thread if needed */ +static void HttpBackend_Add(struct HttpRequest* req, cc_uint8 flags) { + /* Add time based query string parameter to bypass browser cache */ + if (flags & HTTP_FLAG_NOCACHE) { + cc_string url = String_FromRawArray(req->url); + int lo = (int)(startTime), hi = (int)(startTime >> 32); + String_Format2(&url, "?t=%i%i", &hi, &lo); + } + + RequestList_Append(&queuedReqs, req, flags); + Http_StartNextDownload(); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Http component------------------------------------------------------* +*#########################################################################################################################*/ +static void Http_Init(void) { + Http_InitCommon(); + /* If this webpage is https://, browsers deny any http:// downloading */ + httpsOnly = interop_IsHttpsOnly(); + startTime = DateTime_CurrentUTC(); + + RequestList_Init(&queuedReqs); + RequestList_Init(&workingReqs); + RequestList_Init(&processedReqs); +} +#endif diff --git a/src/Http_Worker.c b/src/Http_Worker.c new file mode 100644 index 0000000..260b4c8 --- /dev/null +++ b/src/Http_Worker.c @@ -0,0 +1,1387 @@ +#include "Core.h" +#ifndef CC_BUILD_WEB +#include "_HttpBase.h" + +/* Allocates initial data buffer to store response contents */ +static void Http_BufferInit(struct HttpRequest* req) { + req->progress = 0; + req->_capacity = req->contentLength ? req->contentLength : 1; + req->data = (cc_uint8*)Mem_Alloc(req->_capacity, 1, "http data"); + req->size = 0; +} + +/* Ensures data buffer has enough space left to append amount bytes, reallocates if not */ +static void Http_BufferEnsure(struct HttpRequest* req, cc_uint32 amount) { + cc_uint32 newSize = req->size + amount; + if (newSize <= req->_capacity) return; + + req->_capacity = newSize; + req->data = (cc_uint8*)Mem_Realloc(req->data, newSize, 1, "http data+"); +} + +/* Increases size and updates current progress */ +static void Http_BufferExpanded(struct HttpRequest* req, cc_uint32 read) { + req->size += read; + if (req->contentLength) req->progress = (int)(100.0f * req->size / req->contentLength); +} + + +/*########################################################################################################################* +*---------------------------------------------------Common backend code---------------------------------------------------* +*#########################################################################################################################*/ +static void Http_ParseCookie(struct HttpRequest* req, const cc_string* value) { + cc_string name, data; + int dataEnd; + String_UNSAFE_Separate(value, '=', &name, &data); + /* Cookie is: __cfduid=xyz; expires=abc; path=/; domain=.classicube.net; HttpOnly */ + /* However only the __cfduid=xyz part of the cookie should be stored */ + dataEnd = String_IndexOf(&data, ';'); + if (dataEnd >= 0) data.length = dataEnd; + + EntryList_Set(req->cookies, &name, &data, '='); +} + +static void Http_ParseContentLength(struct HttpRequest* req, const cc_string* value) { + int contentLen = 0; + Convert_ParseInt(value, &contentLen); + + if (contentLen <= 0) return; + req->contentLength = contentLen; +} + +/* Parses a HTTP header */ +static void Http_ParseHeader(struct HttpRequest* req, const cc_string* line) { + static const cc_string httpVersion = String_FromConst("HTTP"); + cc_string name, value, parts[3]; + int numParts; + + /* HTTP[version] [status code] [status reason] */ + if (String_CaselessStarts(line, &httpVersion)) { + numParts = String_UNSAFE_Split(line, ' ', parts, 3); + if (numParts >= 2) Convert_ParseInt(&parts[1], &req->statusCode); + } + /* For all other headers: name: value */ + if (!String_UNSAFE_Separate(line, ':', &name, &value)) return; + + if (String_CaselessEqualsConst(&name, "ETag")) { + String_CopyToRawArray(req->etag, &value); + } else if (String_CaselessEqualsConst(&name, "Content-Length")) { + Http_ParseContentLength(req, &value); + } else if (String_CaselessEqualsConst(&name, "X-Dropbox-Content-Length")) { + /* dropbox stopped returning Content-Length header since switching to chunked transfer */ + /* https://www.dropboxforum.com/t5/Discuss-Dropbox-Developer-API/Dropbox-media-can-t-be-access-by-azure-blob/td-p/575458 */ + Http_ParseContentLength(req, &value); + } else if (String_CaselessEqualsConst(&name, "Last-Modified")) { + String_CopyToRawArray(req->lastModified, &value); + } else if (req->cookies && String_CaselessEqualsConst(&name, "Set-Cookie")) { + Http_ParseCookie(req, &value); + } +} + +/* Adds a http header to the request headers. */ +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value); + +/* Adds all the appropriate headers for a request. */ +static void Http_SetRequestHeaders(struct HttpRequest* req) { + static const cc_string contentType = String_FromConst("application/x-www-form-urlencoded"); + cc_string str, cookies; char cookiesBuffer[1024]; + int i; + + if (req->lastModified[0]) { + str = String_FromRawArray(req->lastModified); + Http_AddHeader(req, "If-Modified-Since", &str); + } + if (req->etag[0]) { + str = String_FromRawArray(req->etag); + Http_AddHeader(req, "If-None-Match", &str); + } + + if (req->data) Http_AddHeader(req, "Content-Type", &contentType); + if (!req->cookies || !req->cookies->count) return; + + String_InitArray(cookies, cookiesBuffer); + for (i = 0; i < req->cookies->count; i++) { + if (i) String_AppendConst(&cookies, "; "); + str = StringsBuffer_UNSAFE_Get(req->cookies, i); + String_AppendString(&cookies, &str); + } + Http_AddHeader(req, "Cookie", &cookies); +} + +/* TODO: Rewrite to use a local variable instead */ +static cc_string* Http_GetUserAgent_UNSAFE(void) { + static char userAgentBuffer[STRING_SIZE]; + static cc_string userAgent; + + String_InitArray(userAgent, userAgentBuffer); + String_AppendConst(&userAgent, GAME_APP_NAME); + String_AppendConst(&userAgent, Platform_AppNameSuffix); + return &userAgent; +} + + +#if defined CC_BUILD_CURL +/*########################################################################################################################* +*-----------------------------------------------------libcurl backend-----------------------------------------------------* +*#########################################################################################################################*/ +#include "Errors.h" +#include +/* === BEGIN CURL HEADERS === */ +typedef void CURL; +struct curl_slist; +typedef int CURLcode; + +#define CURL_GLOBAL_DEFAULT ((1<<0) | (1<<1)) /* SSL and Win32 options */ +#define CURLOPT_WRITEDATA (10000 + 1) +#define CURLOPT_URL (10000 + 2) +#define CURLOPT_ERRORBUFFER (10000 + 10) +#define CURLOPT_WRITEFUNCTION (20000 + 11) +#define CURLOPT_POSTFIELDS (10000 + 15) +#define CURLOPT_USERAGENT (10000 + 18) +#define CURLOPT_HTTPHEADER (10000 + 23) +#define CURLOPT_HEADERDATA (10000 + 29) +#define CURLOPT_VERBOSE (0 + 41) +#define CURLOPT_HEADER (0 + 42) +#define CURLOPT_NOBODY (0 + 44) +#define CURLOPT_POST (0 + 47) +#define CURLOPT_FOLLOWLOCATION (0 + 52) +#define CURLOPT_POSTFIELDSIZE (0 + 60) +#define CURLOPT_SSL_VERIFYPEER (0 + 64) +#define CURLOPT_MAXREDIRS (0 + 68) +#define CURLOPT_HEADERFUNCTION (20000 + 79) +#define CURLOPT_HTTPGET (0 + 80) +#define CURLOPT_SSL_VERIFYHOST (0 + 81) +#define CURLOPT_HTTP_VERSION (0 + 84) + +#define CURL_HTTP_VERSION_1_1 2L /* stick to HTTP 1.1 */ + +#if defined _WIN32 +#define APIENTRY __cdecl +#else +#define APIENTRY +#endif + +static CURLcode (APIENTRY *_curl_global_init)(long flags); +static void (APIENTRY *_curl_global_cleanup)(void); +static CURL* (APIENTRY *_curl_easy_init)(void); +static CURLcode (APIENTRY *_curl_easy_perform)(CURL *c); +static CURLcode (APIENTRY *_curl_easy_setopt)(CURL *c, int opt, ...); +static void (APIENTRY *_curl_easy_cleanup)(CURL* c); +static void (APIENTRY *_curl_slist_free_all)(struct curl_slist* l); +static struct curl_slist* (APIENTRY *_curl_slist_append)(struct curl_slist* l, const char* v); +static const char* (APIENTRY *_curl_easy_strerror)(CURLcode res); +/* === END CURL HEADERS === */ + +#if defined CC_BUILD_WIN +static const cc_string curlLib = String_FromConst("libcurl.dll"); +static const cc_string curlAlt = String_FromConst("curl.dll"); +#elif defined CC_BUILD_DARWIN +static const cc_string curlLib = String_FromConst("libcurl.4.dylib"); +static const cc_string curlAlt = String_FromConst("libcurl.dylib"); +#elif defined CC_BUILD_NETBSD +static const cc_string curlLib = String_FromConst("libcurl.so"); +static const cc_string curlAlt = String_FromConst("/usr/pkg/lib/libcurl.so"); +#elif defined CC_BUILD_BSD +static const cc_string curlLib = String_FromConst("libcurl.so"); +static const cc_string curlAlt = String_FromConst("libcurl.so"); +#elif defined CC_BUILD_SERENITY +static const cc_string curlLib = String_FromConst("/usr/local/lib/libcurl.so"); +static const cc_string curlAlt = String_FromConst("/usr/local/lib/libcurl.so"); +#elif defined CC_BUILD_OS2 +static const cc_string curlLib = String_FromConst("/@unixroot/usr/lib/curl4.dll"); +static const cc_string curlAlt = String_FromConst("/@unixroot/usr/local/lib/curl4.dll"); +#else +static const cc_string curlLib = String_FromConst("libcurl.so.4"); +static const cc_string curlAlt = String_FromConst("libcurl.so.3"); +#endif + +static cc_bool LoadCurlFuncs(void) { + static const struct DynamicLibSym funcs[] = { +#if !defined CC_BUILD_OS2 + DynamicLib_Sym(curl_global_init), DynamicLib_Sym(curl_global_cleanup), + DynamicLib_Sym(curl_easy_init), DynamicLib_Sym(curl_easy_perform), + DynamicLib_Sym(curl_easy_setopt), DynamicLib_Sym(curl_easy_cleanup), + DynamicLib_Sym(curl_slist_free_all), DynamicLib_Sym(curl_slist_append) +#else + DynamicLib_SymC(curl_global_init), DynamicLib_SymC(curl_global_cleanup), + DynamicLib_SymC(curl_easy_init), DynamicLib_SymC(curl_easy_perform), + DynamicLib_SymC(curl_easy_setopt), DynamicLib_SymC(curl_easy_cleanup), + DynamicLib_SymC(curl_slist_free_all), DynamicLib_SymC(curl_slist_append) +#endif + }; + cc_bool success; + void* lib; + + success = DynamicLib_LoadAll(&curlLib, funcs, Array_Elems(funcs), &lib); + if (!lib) { + success = DynamicLib_LoadAll(&curlAlt, funcs, Array_Elems(funcs), &lib); + } + + /* Non-essential function missing in older curl versions */ + _curl_easy_strerror = DynamicLib_Get2(lib, "curl_easy_strerror"); + return success; +} + +static CURL* curl; +static cc_bool curlSupported, curlVerbose; + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + const char* err; + + if (!_curl_easy_strerror) return false; + err = _curl_easy_strerror((CURLcode)res); + if (!err) return false; + + String_AppendConst(dst, err); + return true; +} + +static void HttpBackend_Init(void) { + static const cc_string msg = String_FromConst("Failed to init libcurl. All HTTP requests will therefore fail."); + CURLcode res; + + if (!LoadCurlFuncs()) { Logger_WarnFunc(&msg); return; } + res = _curl_global_init(CURL_GLOBAL_DEFAULT); + if (res) { Logger_SimpleWarn(res, "initing curl"); return; } + curl = _curl_easy_init(); + if (!curl) { Logger_SimpleWarn(res, "initing curl_easy"); return; } + + curlSupported = true; + curlVerbose = Options_GetBool("curl-verbose", false); +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + cc_string tmp; char tmpBuffer[1024]; + String_InitArray_NT(tmp, tmpBuffer); + String_Format2(&tmp, "%c: %s", key, value); + + tmp.buffer[tmp.length] = '\0'; + req->meta = _curl_slist_append((struct curl_slist*)req->meta, tmp.buffer); +} + +/* Processes a HTTP header downloaded from the server */ +static size_t Http_ProcessHeader(char* buffer, size_t size, size_t nitems, void* userdata) { + struct HttpRequest* req = (struct HttpRequest*)userdata; + size_t len = nitems; + cc_string line; + /* line usually has \r\n at end */ + if (len && buffer[len - 1] == '\n') len--; + if (len && buffer[len - 1] == '\r') len--; + + line = String_Init(buffer, len, len); + Http_ParseHeader(req, &line); + return nitems; +} + +/* Processes a chunk of data downloaded from the web server */ +static size_t Http_ProcessData(char *buffer, size_t size, size_t nitems, void* userdata) { + struct HttpRequest* req = (struct HttpRequest*)userdata; + + if (!req->_capacity) Http_BufferInit(req); + Http_BufferEnsure(req, nitems); + + Mem_Copy(&req->data[req->size], buffer, nitems); + Http_BufferExpanded(req, nitems); + return nitems; +} + +/* Sets general curl options for a request */ +static void Http_SetCurlOpts(struct HttpRequest* req) { + _curl_easy_setopt(curl, CURLOPT_USERAGENT, GAME_APP_NAME); + _curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + _curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 20L); + _curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + + _curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, Http_ProcessHeader); + _curl_easy_setopt(curl, CURLOPT_HEADERDATA, req); + _curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Http_ProcessData); + _curl_easy_setopt(curl, CURLOPT_WRITEDATA, req); + + if (curlVerbose) _curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + + if (httpsVerify) return; + _curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + char urlStr[NATIVE_STR_LEN]; + void* post_data = req->data; + CURLcode res; + if (!curlSupported) return ERR_NOT_SUPPORTED; + + req->meta = NULL; + Http_SetRequestHeaders(req); + _curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req->meta); + + Http_SetCurlOpts(req); + String_EncodeUtf8(urlStr, url); + _curl_easy_setopt(curl, CURLOPT_URL, urlStr); + + if (req->requestType == REQUEST_TYPE_HEAD) { + _curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } else if (req->requestType == REQUEST_TYPE_POST) { + _curl_easy_setopt(curl, CURLOPT_POST, 1L); + _curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, req->size); + _curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->data); + + /* per curl docs, we must persist POST data until request finishes */ + req->data = NULL; + req->size = 0; + } else { + /* Undo POST/HEAD state */ + _curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } + + /* must be at least CURL_ERROR_SIZE (256) in size */ + req->error = Mem_TryAllocCleared(257, 1); + _curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, req->error); + /* TODO stackalloc instead and then copy to dynamic array later? */ + /* probably not worth the extra complexity though */ + + req->_capacity = 0; + req->progress = HTTP_PROGRESS_FETCHING_DATA; + res = _curl_easy_perform(curl); + req->progress = 100; + + /* Free error string if it isn't needed */ + if (req->error && !req->error[0]) { + Mem_Free(req->error); + req->error = NULL; + } + + _curl_slist_free_all((struct curl_slist*)req->meta); + /* can free now that request has finished */ + Mem_Free(post_data); + _curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, NULL); + return res; +} +#elif defined CC_BUILD_HTTPCLIENT +#include "Errors.h" +#include "PackedCol.h" +#include "SSL.h" + +/*########################################################################################################################* +*---------------------------------------------------------HttpUrl---------------------------------------------------------* +*#########################################################################################################################*/ +/* Components of a URL */ +struct HttpUrl { + cc_bool https; /* Whether HTTPS or just HTTP protocol */ + cc_string address; /* Address of server (e.g. "classicube.net:8080") */ + cc_string resource; /* Path being accessed (and query string) */ + char _addressBuffer[STRING_SIZE + 8]; + char _resourceBuffer[STRING_SIZE * 4]; +}; + +static void HttpUrl_EncodeUrl(cc_string* dst, const cc_string* src) { + cc_uint8 data[4]; + int i, len; + char c; + + for (i = 0; i < src->length; i++) { + c = src->buffer[i]; + len = Convert_CP437ToUtf8(c, data); + + /* URL path/query must not be URL encoded (it normally would be) */ + if (c == '/' || c == '?' || c == '=') { + String_Append(dst, c); + } else { + Http_UrlEncode(dst, data, len); + } + } +} + +/* Splits up the components of a URL */ +static void HttpUrl_Parse(const cc_string* src, struct HttpUrl* url) { + cc_string scheme, path, addr, resource; + /* URL is of form [scheme]://[server host]:[server port]/[resource] */ + /* For simplicity, parsed as [scheme]://[server address]/[resource] */ + int idx = String_IndexOfConst(src, "://"); + + scheme = idx == -1 ? String_Empty : String_UNSAFE_Substring(src, 0, idx); + path = idx == -1 ? *src : String_UNSAFE_SubstringAt(src, idx + 3); + + url->https = String_CaselessEqualsConst(&scheme, "https"); + String_UNSAFE_Separate(&path, '/', &addr, &resource); + + String_InitArray(url->address, url->_addressBuffer); + String_Copy(&url->address, &addr); + + String_InitArray(url->resource, url->_resourceBuffer); + String_Append(&url->resource, '/'); + /* Address may have unicode characters - need to percent encode them */ + HttpUrl_EncodeUrl(&url->resource, &resource); +} + +static cc_result HttpUrl_ResolveRedirect(struct HttpUrl* parts, const cc_string* url) { + /* absolute URL */ + if (String_IndexOfConst(url, "http://") == 0 || String_IndexOfConst(url, "https://") == 0) { + HttpUrl_Parse(url, parts); + return 0; + } + + /* Root relative URL */ + if (url->buffer[0] == '/' && (url->length == 1 || url->buffer[1] != '/')) { + parts->resource.length = 0; + HttpUrl_EncodeUrl(&parts->resource, url); + return 0; + } + + /* TODO scheme relative or relative URL or invalid */ + return HTTP_ERR_RELATIVE; +} + + +/*########################################################################################################################* +*------------------------------------------------------HttpConnection-----------------------------------------------------* +*#########################################################################################################################*/ +struct HttpConnection { + cc_socket socket; + void* sslCtx; + cc_bool valid; +}; + +static void HttpConnection_Close(struct HttpConnection* conn) { + if (conn->sslCtx) { + SSL_Free(conn->sslCtx); + conn->sslCtx = NULL; + } + + if (conn->socket != -1) { + Socket_Close(conn->socket); + conn->socket = -1; + } + conn->valid = false; +} + +static void ExtractHostPort(const struct HttpUrl* url, cc_string* host, cc_string* port) { + /* address can have the form of either "host" or "host:port" */ + /* Slightly more complicated because IPv6 hosts can be e.g. [::1] */ + const cc_string* addr = &url->address; + int idx = String_LastIndexOf(addr, ':'); + + if (idx == -1) { + *host = *addr; + *port = String_Empty; + } else { + *host = String_UNSAFE_Substring(addr, 0, idx); + *port = String_UNSAFE_SubstringAt(addr, idx + 1); + } +} + +static cc_result HttpConnection_Open(struct HttpConnection* conn, const struct HttpUrl* url) { + cc_string host, port; + cc_uint16 portNum; + cc_result res; + cc_sockaddr addrs[SOCKET_MAX_ADDRS]; + int i, numValidAddrs; + + ExtractHostPort(url, &host, &port); + if (!Convert_ParseUInt16(&port, &portNum)) { + portNum = url->https ? 443 : 80; + } + + conn->socket = -1; + conn->sslCtx = NULL; + + if ((res = Socket_ParseAddress(&host, portNum, addrs, &numValidAddrs))) return res; + res = ERR_INVALID_ARGUMENT; /* in case 0 valid addresses */ + + /* TODO: Connect in parallel instead of serial, but that's a lot of work */ + for (i = 0; i < numValidAddrs; i++) + { + res = Socket_Connect(&conn->socket, &addrs[i], false); + if (!res) break; + + HttpConnection_Close(conn); + } + if (res) return res; + + conn->valid = true; + if (!url->https) return 0; + return SSL_Init(conn->socket, &host, &conn->sslCtx); +} + +static cc_result HttpConnection_Read(struct HttpConnection* conn, cc_uint8* data, cc_uint32 count, cc_uint32* read) { + if (conn->sslCtx) + return SSL_Read(conn->sslCtx, data, count, read); + + return Socket_Read(conn->socket, data, count, read); +} + +static cc_result HttpConnection_Write(struct HttpConnection* conn, const cc_uint8* data, cc_uint32 count) { + if (conn->sslCtx) + return SSL_WriteAll(conn->sslCtx, data, count); + + return Socket_WriteAll(conn->socket, data, count); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Connection Pool-----------------------------------------------------* +*#########################################################################################################################*/ +static struct ConnectionPoolEntry { + struct HttpConnection conn; + cc_string addr; + char addrBuffer[STRING_SIZE]; + cc_bool https; +} connection_pool[10]; + +static cc_result ConnectionPool_Insert(int i, struct HttpConnection** conn, const struct HttpUrl* url) { + struct ConnectionPoolEntry* e = &connection_pool[i]; + *conn = &e->conn; + + String_InitArray(e->addr, e->addrBuffer); + String_Copy(&e->addr, &url->address); + e->https = url->https; + return HttpConnection_Open(&e->conn, url); +} + +static cc_result ConnectionPool_Open(struct HttpConnection** conn, const struct HttpUrl* url) { + struct ConnectionPoolEntry* e; + int i; + + for (i = 0; i < Array_Elems(connection_pool); i++) + { + e = &connection_pool[i]; + if (e->conn.valid && e->https == url->https && String_Equals(&e->addr, &url->address)) { + *conn = &connection_pool[i].conn; + return 0; + } + } + + for (i = 0; i < Array_Elems(connection_pool); i++) + { + e = &connection_pool[i]; + if (!e->conn.valid) return ConnectionPool_Insert(i, conn, url); + } + + /* TODO: Should we be consistent in which entry gets evicted? */ + i = (cc_uint8)Stopwatch_Measure() % Array_Elems(connection_pool); + HttpConnection_Close(&connection_pool[i].conn); + return ConnectionPool_Insert(i, conn, url); +} + + +/*########################################################################################################################* +*--------------------------------------------------------HttpClient-------------------------------------------------------* +*#########################################################################################################################*/ +enum HTTP_RESPONSE_STATE { + HTTP_RESPONSE_STATE_INITIAL, + HTTP_RESPONSE_STATE_HEADER, + HTTP_RESPONSE_STATE_DATA, + HTTP_RESPONSE_STATE_CHUNK_HEADER, + HTTP_RESPONSE_STATE_CHUNK_END_R, + HTTP_RESPONSE_STATE_CHUNK_END_N, + HTTP_RESPONSE_STATE_CHUNK_TRAILERS, + HTTP_RESPONSE_STATE_DONE +}; +#define HTTP_HEADER_MAX_LENGTH 4096 +#define HTTP_LOCATION_MAX_LENGTH 256 + +struct HttpClientState { + enum HTTP_RESPONSE_STATE state; + struct HttpConnection* conn; + struct HttpRequest* req; + cc_uint32 dataLeft; /* Number of bytes still to read from the current chunk or body */ + int chunked; + cc_bool autoClose; + cc_string header, location; + struct HttpUrl url; + char _headerBuffer[HTTP_HEADER_MAX_LENGTH]; + char _locationBuffer[HTTP_LOCATION_MAX_LENGTH]; +}; + +static void HttpClientState_Reset(struct HttpClientState* state) { + state->state = HTTP_RESPONSE_STATE_INITIAL; + state->chunked = 0; + state->dataLeft = 0; + state->autoClose = false; + String_InitArray(state->header, state->_headerBuffer); + String_InitArray(state->location, state->_locationBuffer); +} + +static void HttpClientState_Init(struct HttpClientState* state) { + HttpClientState_Reset(state); +} + + +static void HttpClient_Serialise(struct HttpClientState* state) { + static const char* verbs[] = { "GET", "HEAD", "POST" }; + + struct HttpRequest* req = state->req; + cc_string* buffer = (cc_string*)req->meta; + /* TODO move to other functions */ + /* Write request message headers */ + String_Format2(buffer, "%c %s HTTP/1.1\r\n", + verbs[req->requestType], &state->url.resource); + + Http_AddHeader(req, "Host", &state->url.address); + Http_AddHeader(req, "User-Agent", Http_GetUserAgent_UNSAFE()); + if (req->data) String_Format1(buffer, "Content-Length: %i\r\n", &req->size); + + Http_SetRequestHeaders(req); + String_AppendConst(buffer, "\r\n"); + + /* Write request message body */ + if (req->data) { + String_AppendAll(buffer, req->data, req->size); + HttpRequest_Free(req); + } /* TODO post redirect handling */ +} + +static cc_result HttpClient_SendRequest(struct HttpClientState* state) { + char inputBuffer[16384]; + cc_string inputMsg; + + String_InitArray(inputMsg, inputBuffer); + state->req->meta = &inputMsg; + state->req->progress = HTTP_PROGRESS_FETCHING_DATA; + HttpClient_Serialise(state); + + return HttpConnection_Write(state->conn, (cc_uint8*)inputBuffer, inputMsg.length); +} + + +static void HttpClient_ParseHeader(struct HttpClientState* state, const cc_string* line) { + static const cc_string HTTP_10_VERSION = String_FromConst("HTTP/1.0"); + cc_string name, value; + /* HTTP 1.0 defaults to auto closing connection */ + if (String_CaselessStarts(line, &HTTP_10_VERSION)) state->autoClose = true; + + /* name: value */ + if (!String_UNSAFE_Separate(line, ':', &name, &value)) return; + + if (String_CaselessEqualsConst(&name, "Transfer-Encoding")) { + state->chunked = String_CaselessEqualsConst(&value, "chunked"); + } else if (String_CaselessEqualsConst(&name, "Location")) { + String_Copy(&state->location, &value); + } else if (String_CaselessEqualsConst(&name, "Connection")) { + if (String_CaselessEqualsConst(&value, "keep-alive")) state->autoClose = false; + if (String_CaselessEqualsConst(&value, "close")) state->autoClose = true; + } +} + +/* RFC 7230, section 3.3.3 - Message Body Length */ +static cc_bool HttpClient_HasBody(struct HttpRequest* req) { + /* HEAD responses never have a message body */ + if (req->requestType == REQUEST_TYPE_HEAD) return false; + /* 1XX (Information) responses don't have message body */ + if (req->statusCode >= 100 && req->statusCode <= 199) return false; + /* 204 (No Content) and 304 (Not Modified) also don't */ + if (req->statusCode == 204 || req->statusCode == 304) return false; + + return true; +} + +static int HttpClient_BeginBody(struct HttpRequest* req, struct HttpClientState* state) { + if (!HttpClient_HasBody(req)) + return HTTP_RESPONSE_STATE_DONE; + + if (state->chunked) { + Http_BufferInit(req); + return HTTP_RESPONSE_STATE_CHUNK_HEADER; + } + if (req->contentLength) { + Http_BufferInit(req); + return HTTP_RESPONSE_STATE_DATA; + } + /* Zero length response */ + return HTTP_RESPONSE_STATE_DONE; +} + +/* RFC 7230, section 4.1 - Chunked Transfer Coding */ +static int HttpClient_GetChunkLength(const cc_string* line) { + int length = 0, i, part; + + for (i = 0; i < line->length; i++) + { + char c = line->buffer[i]; + /* RFC 7230, section 4.1.1 - Chunk Extensions */ + if (c == ';') break; + + part = PackedCol_DeHex(c); + if (part == -1) return -1; + length = (length << 4) | part; + } + return length; +} + +/* https://httpwg.org/specs/rfc7230.html */ +static cc_result HttpClient_Process(struct HttpClientState* state, char* buffer, int total) { + struct HttpRequest* req = state->req; + cc_uint32 left, avail, read; + int offset = 0, chunkLen; + + while (offset < total) { + switch (state->state) { + case HTTP_RESPONSE_STATE_INITIAL: + state->state = HTTP_RESPONSE_STATE_HEADER; + break; + + case HTTP_RESPONSE_STATE_HEADER: + { + for (; offset < total;) + { + char c = buffer[offset++]; + if (c == '\r') continue; + if (c != '\n') { + /* Warn when a header would be truncated */ + if (state->header.length == HTTP_HEADER_MAX_LENGTH) + return HTTP_ERR_TRUNCATED; + + String_Append(&state->header, c); + continue; + } + + /* Zero length header = end of message headers */ + if (state->header.length == 0) { + state->state = HttpClient_BeginBody(req, state); + + /* The rest of the request body is just content/data */ + if (state->state == HTTP_RESPONSE_STATE_DATA) { + Http_BufferEnsure(req, req->contentLength); + state->dataLeft = req->contentLength; + } + break; + } + + Http_ParseHeader(state->req, &state->header); + HttpClient_ParseHeader(state, &state->header); + state->header.length = 0; + } + } + break; + + case HTTP_RESPONSE_STATE_DATA: + { + left = total - offset; + avail = state->dataLeft; + read = min(left, avail); + + Mem_Copy(req->data + req->size, buffer + offset, read); + Http_BufferExpanded(req, read); state->dataLeft -= read; + offset += read; + + if (!state->dataLeft) { + state->state = state->chunked ? HTTP_RESPONSE_STATE_CHUNK_END_R : HTTP_RESPONSE_STATE_DONE; + } + } + break; + + + /* RFC 7230, section 4.1 - Chunked Transfer Coding */ + case HTTP_RESPONSE_STATE_CHUNK_HEADER: + { + for (; offset < total;) + { + char c = buffer[offset++]; + if (c == '\r') continue; + if (c != '\n') { String_Append(&state->header, c); continue; } + + chunkLen = HttpClient_GetChunkLength(&state->header); + if (chunkLen < 0) return HTTP_ERR_CHUNK_SIZE; + state->header.length = 0; + + if (chunkLen == 0) { + state->state = HTTP_RESPONSE_STATE_CHUNK_TRAILERS; + } else { + state->state = HTTP_RESPONSE_STATE_DATA; + Http_BufferEnsure(req, chunkLen); + state->dataLeft = chunkLen; + } + break; + } + } + break; + + /* Chunks are terminated by \r\n */ + case HTTP_RESPONSE_STATE_CHUNK_END_R: + if (buffer[offset++] != '\r') return ERR_INVALID_ARGUMENT; + + state->state = HTTP_RESPONSE_STATE_CHUNK_END_N; + break; + + case HTTP_RESPONSE_STATE_CHUNK_END_N: + if (buffer[offset++] != '\n') return ERR_INVALID_ARGUMENT; + + state->state = HTTP_RESPONSE_STATE_CHUNK_HEADER; + break; + + /* RFC 7230, section 4.1.2 - Chunked Trailer Part */ + case HTTP_RESPONSE_STATE_CHUNK_TRAILERS: + { + for (; offset < total;) + { + char c = buffer[offset++]; + if (c == '\r') continue; + if (c != '\n') { String_Append(&state->header, c); continue; } + + /* Zero length header = end of message trailers */ + if (state->header.length == 0) { + state->state = HTTP_RESPONSE_STATE_DONE; + break; + } + state->header.length = 0; + } + } + break; + + default: + return 0; + } + } + return 0; +} + +#define INPUT_BUFFER_LEN 8192 +static cc_result HttpClient_ParseResponse(struct HttpClientState* state) { + struct HttpRequest* req = state->req; + cc_uint8 buffer[INPUT_BUFFER_LEN]; + cc_uint8* dst; + cc_uint32 total; + cc_result res; + + for (;;) + { + dst = state->dataLeft > INPUT_BUFFER_LEN ? (req->data + req->size) : buffer; + res = HttpConnection_Read(state->conn, dst, INPUT_BUFFER_LEN, &total); + if (res) return res; + + if (total == 0) { + Platform_Log1("Http read unexpectedly returned 0 in state %i", &state->state); + return state->state == HTTP_RESPONSE_STATE_INITIAL ? HTTP_ERR_NO_RESPONSE : ERR_END_OF_STREAM; + } + + if (dst != buffer) { + /* When there is more than INPUT_BUFFER_LEN bytes of unread data/content, */ + /* there is no need to run the HTTP client state machine - just read directly */ + /* into the output buffer to avoid a pointless Mem_Copy call */ + Http_BufferExpanded(req, total); + state->dataLeft -= total; + } else { + res = HttpClient_Process(state, (char*)buffer, total); + } + + if (res) return res; + if (state->state == HTTP_RESPONSE_STATE_DONE) return 0; + } +} + +static cc_bool HttpClient_IsRedirect(struct HttpRequest* req) { + return req->statusCode >= 300 && req->statusCode <= 399 && req->statusCode != 304; +} + +static cc_result HttpClient_HandleRedirect(struct HttpClientState* state) { + cc_result res = HttpUrl_ResolveRedirect(&state->url, &state->location); + if (res) return res; + + HttpRequest_Free(state->req); + Platform_Log1(" Redirecting to: %s", &state->location); + state->req->contentLength = 0; /* TODO */ + return 0; +} + + +/*########################################################################################################################* +*-----------------------------------------------Http backend implementation-----------------------------------------------* +*#########################################################################################################################*/ +static void HttpBackend_Init(void) { + SSLBackend_Init(httpsVerify); + //httpOnly = true; // TODO: insecure +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + String_Format2((cc_string*)req->meta, "%c:%s\r\n", key, value); +} + +static cc_result HttpBackend_PerformRequest(struct HttpClientState* state) { + cc_result res; + + res = ConnectionPool_Open(&state->conn, &state->url); + if (res) { HttpConnection_Close(state->conn); return res; } + + res = HttpClient_SendRequest(state); + if (res) { HttpConnection_Close(state->conn); return res; } + + res = HttpClient_ParseResponse(state); + if (res) HttpConnection_Close(state->conn); + + return res; +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* urlStr) { + struct HttpClientState state; + cc_bool retried = false; + int redirects = 0; + cc_result res; + + HttpClientState_Init(&state); + HttpUrl_Parse(urlStr, &state.url); + state.req = req; + + for (;;) { + res = HttpBackend_PerformRequest(&state); + /* TODO: Can we handle this while preserving the TCP connection */ + if (res == SSL_ERR_CONTEXT_DEAD && !retried) { + Platform_LogConst("Resetting connection due to SSL context being dropped.."); + res = HttpBackend_PerformRequest(&state); + retried = true; + } + if (res == HTTP_ERR_NO_RESPONSE && !retried) { + Platform_LogConst("Resetting connection due to empty response.."); + res = HttpBackend_PerformRequest(&state); + retried = true; + } + + if (res || !HttpClient_IsRedirect(req)) break; + if (redirects >= 20) return HTTP_ERR_REDIRECTS; + + /* TODO FOLLOW LOCATION PROPERLY */ + redirects++; + res = HttpClient_HandleRedirect(&state); + if (res) break; + HttpClientState_Reset(&state); + } + return res; +} + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return SSLBackend_DescribeError(res, dst); +} +#elif defined CC_BUILD_ANDROID +/*########################################################################################################################* +*-----------------------------------------------------Android backend-----------------------------------------------------* +*#########################################################################################################################*/ +struct HttpRequest* java_req; +static jmethodID JAVA_httpInit, JAVA_httpSetHeader, JAVA_httpPerform, JAVA_httpSetData; +static jmethodID JAVA_httpDescribeError; + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + char buffer[NATIVE_STR_LEN]; + cc_string err; + JNIEnv* env; + jvalue args[1]; + jobject obj; + + JavaGetCurrentEnv(env); + args[0].i = res; + obj = JavaSCall_Obj(env, JAVA_httpDescribeError, args); + if (!obj) return false; + + err = JavaGetString(env, obj, buffer); + String_AppendString(dst, &err); + (*env)->DeleteLocalRef(env, obj); + return true; +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + JNIEnv* env; + jvalue args[2]; + + JavaGetCurrentEnv(env); + args[0].l = JavaMakeConst(env, key); + args[1].l = JavaMakeString(env, value); + + JavaSCall_Void(env, JAVA_httpSetHeader, args); + (*env)->DeleteLocalRef(env, args[0].l); + (*env)->DeleteLocalRef(env, args[1].l); +} + +/* Processes a HTTP header downloaded from the server */ +static void JNICALL java_HttpParseHeader(JNIEnv* env, jobject o, jstring header) { + char buffer[NATIVE_STR_LEN]; + cc_string line = JavaGetString(env, header, buffer); + Http_ParseHeader(java_req, &line); +} + +/* Processes a chunk of data downloaded from the web server */ +static void JNICALL java_HttpAppendData(JNIEnv* env, jobject o, jbyteArray arr, jint len) { + struct HttpRequest* req = java_req; + if (!req->_capacity) Http_BufferInit(req); + + Http_BufferEnsure(req, len); + (*env)->GetByteArrayRegion(env, arr, 0, len, (jbyte*)(&req->data[req->size])); + Http_BufferExpanded(req, len); +} + +static const JNINativeMethod methods[] = { + { "httpParseHeader", "(Ljava/lang/String;)V", java_HttpParseHeader }, + { "httpAppendData", "([BI)V", java_HttpAppendData } +}; +static void CacheMethodRefs(JNIEnv* env) { + JAVA_httpInit = JavaGetSMethod(env, "httpInit", "(Ljava/lang/String;Ljava/lang/String;)I"); + JAVA_httpSetHeader = JavaGetSMethod(env, "httpSetHeader", "(Ljava/lang/String;Ljava/lang/String;)V"); + JAVA_httpPerform = JavaGetSMethod(env, "httpPerform", "()I"); + JAVA_httpSetData = JavaGetSMethod(env, "httpSetData", "([B)I"); + + JAVA_httpDescribeError = JavaGetSMethod(env, "httpDescribeError", "(I)Ljava/lang/String;"); +} + +static void HttpBackend_Init(void) { + JNIEnv* env; + JavaGetCurrentEnv(env); + JavaRegisterNatives(env, methods); + CacheMethodRefs(env); +} + +static cc_result Http_InitReq(JNIEnv* env, struct HttpRequest* req, cc_string* url) { + static const char* verbs[3] = { "GET", "HEAD", "POST" }; + jvalue args[2]; + jint res; + + args[0].l = JavaMakeString(env, url); + args[1].l = JavaMakeConst(env, verbs[req->requestType]); + + res = JavaSCall_Int(env, JAVA_httpInit, args); + (*env)->DeleteLocalRef(env, args[0].l); + (*env)->DeleteLocalRef(env, args[1].l); + return res; +} + +static cc_result Http_SetData(JNIEnv* env, struct HttpRequest* req) { + jvalue args[1]; + jint res; + + args[0].l = JavaMakeBytes(env, req->data, req->size); + res = JavaSCall_Int(env, JAVA_httpSetData, args); + (*env)->DeleteLocalRef(env, args[0].l); + return res; +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + JNIEnv* env; + jint res; + + JavaGetCurrentEnv(env); + if ((res = Http_InitReq(env, req, url))) return res; + java_req = req; + + Http_SetRequestHeaders(req); + Http_AddHeader(req, "User-Agent", Http_GetUserAgent_UNSAFE()); + if (req->data && (res = Http_SetData(env, req))) return res; + + req->_capacity = 0; + req->progress = HTTP_PROGRESS_FETCHING_DATA; + res = JavaSCall_Int(env, JAVA_httpPerform, NULL); + req->progress = 100; + return res; +} +#elif defined CC_BUILD_CFNETWORK +/*########################################################################################################################* +*----------------------------------------------------CFNetwork backend----------------------------------------------------* +*#########################################################################################################################*/ +#include "Errors.h" +#include +#include + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +static void HttpBackend_Init(void) { + +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + char tmp[NATIVE_STR_LEN]; + CFStringRef keyCF, valCF; + CFHTTPMessageRef msg = (CFHTTPMessageRef)req->meta; + String_EncodeUtf8(tmp, value); + + keyCF = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8); + valCF = CFStringCreateWithCString(NULL, tmp, kCFStringEncodingUTF8); + CFHTTPMessageSetHeaderFieldValue(msg, keyCF, valCF); + CFRelease(keyCF); + CFRelease(valCF); +} + +static void Http_CheckHeader(const void* k, const void* v, void* ctx) { + cc_string line; char lineBuffer[2048]; + char keyBuf[128] = { 0 }; + char valBuf[1024] = { 0 }; + String_InitArray(line, lineBuffer); + + CFStringGetCString((CFStringRef)k, keyBuf, sizeof(keyBuf), kCFStringEncodingUTF8); + CFStringGetCString((CFStringRef)v, valBuf, sizeof(valBuf), kCFStringEncodingUTF8); + + String_Format2(&line, "%c:%c", keyBuf, valBuf); + Http_ParseHeader((struct HttpRequest*)ctx, &line); + ctx = NULL; +} + +static cc_result ParseResponseHeaders(struct HttpRequest* req, CFReadStreamRef stream) { + CFHTTPMessageRef response = CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); + if (!response) return ERR_INVALID_ARGUMENT; + + CFDictionaryRef headers = CFHTTPMessageCopyAllHeaderFields(response); + CFDictionaryApplyFunction(headers, Http_CheckHeader, req); + req->statusCode = CFHTTPMessageGetResponseStatusCode(response); + + CFRelease(headers); + CFRelease(response); + return 0; +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + static CFStringRef verbs[] = { CFSTR("GET"), CFSTR("HEAD"), CFSTR("POST") }; + cc_bool gotHeaders = false; + char tmp[NATIVE_STR_LEN]; + CFHTTPMessageRef request; + CFStringRef urlCF; + CFURLRef urlRef; + cc_result result = 0; + + String_EncodeUtf8(tmp, url); + urlCF = CFStringCreateWithCString(NULL, tmp, kCFStringEncodingUTF8); + urlRef = CFURLCreateWithString(NULL, urlCF, NULL); + // TODO e.g. "http://www.example.com/skin/1 2.png" causes this to return null + // TODO release urlCF + if (!urlRef) return ERR_INVALID_DATA_URL; + + request = CFHTTPMessageCreateRequest(NULL, verbs[req->requestType], urlRef, kCFHTTPVersion1_1); + req->meta = request; + Http_SetRequestHeaders(req); + Http_AddHeader(req, "User-Agent", Http_GetUserAgent_UNSAFE()); + CFRelease(urlRef); + + if (req->data && req->size) { + CFDataRef body = CFDataCreate(NULL, req->data, req->size); + CFHTTPMessageSetBody(request, body); + CFRelease(body); /* TODO: ???? */ + + req->data = NULL; + req->size = 0; + Mem_Free(req->data); + } + + CFReadStreamRef stream = CFReadStreamCreateForHTTPRequest(NULL, request); + CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue); + //CFHTTPReadStreamSetRedirectsAutomatically(stream, TRUE); + CFReadStreamOpen(stream); + UInt8 buf[1024]; + + for (;;) { + CFIndex read = CFReadStreamRead(stream, buf, sizeof(buf)); + if (read <= 0) break; + + // reading headers before for loop doesn't work + if (!gotHeaders) { + gotHeaders = true; + if ((result = ParseResponseHeaders(req, stream))) break; + } + + if (!req->_capacity) Http_BufferInit(req); + Http_BufferEnsure(req, read); + + Mem_Copy(&req->data[req->size], buf, read); + Http_BufferExpanded(req, read); + } + + if (!gotHeaders) + result = ParseResponseHeaders(req, stream); + + //Thread_Sleep(1000); + CFRelease(request); + return result; +} +#elif !defined CC_BUILD_NETWORKING +/*########################################################################################################################* +*------------------------------------------------------Null backend-------------------------------------------------------* +*#########################################################################################################################*/ +#include "Errors.h" + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +static void HttpBackend_Init(void) { } + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { } + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + req->progress = 100; + return ERR_NOT_SUPPORTED; +} +#endif + + +static void* workerWaitable; +static void* workerThread; + +static void* pendingMutex; +static struct RequestList pendingReqs; + +static void* curRequestMutex; +static struct HttpRequest http_curRequest; + + +/*########################################################################################################################* +*----------------------------------------------------Http public api------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Http_GetResult(int reqID, struct HttpRequest* item) { + int i; + Mutex_Lock(processedMutex); + { + i = RequestList_Find(&processedReqs, reqID); + if (i >= 0) HttpRequest_Copy(item, &processedReqs.entries[i]); + if (i >= 0) RequestList_RemoveAt(&processedReqs, i); + } + Mutex_Unlock(processedMutex); + return i >= 0; +} + +cc_bool Http_GetCurrent(int* reqID, int* progress) { + Mutex_Lock(curRequestMutex); + { + *reqID = http_curRequest.id; + *progress = http_curRequest.progress; + } + Mutex_Unlock(curRequestMutex); + return *reqID != 0; +} + +int Http_CheckProgress(int reqID) { + int curReqID, progress; + Http_GetCurrent(&curReqID, &progress); + + if (reqID != curReqID) progress = HTTP_PROGRESS_NOT_WORKING_ON; + return progress; +} + +void Http_ClearPending(void) { + Mutex_Lock(pendingMutex); + { + RequestList_Free(&pendingReqs); + } + Mutex_Unlock(pendingMutex); +} + +void Http_TryCancel(int reqID) { + Mutex_Lock(pendingMutex); + { + RequestList_TryFree(&pendingReqs, reqID); + } + Mutex_Unlock(pendingMutex); + + Mutex_Lock(processedMutex); + { + RequestList_TryFree(&processedReqs, reqID); + } + Mutex_Unlock(processedMutex); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Http worker---------------------------------------------------------* +*#########################################################################################################################*/ +/* Sets up state to begin a http request */ +static void PrepareCurrentRequest(struct HttpRequest* req, cc_string* url) { + static const char* verbs[] = { "GET", "HEAD", "POST" }; + Http_GetUrl(req, url); + Platform_Log2("Fetching %s (%c)", url, verbs[req->requestType]); + /* TODO change to verbs etc */ + + Mutex_Lock(curRequestMutex); + { + HttpRequest_Copy(&http_curRequest, req); + http_curRequest.progress = HTTP_PROGRESS_MAKING_REQUEST; + } + Mutex_Unlock(curRequestMutex); +} + +static void PerformRequest(struct HttpRequest* req, cc_string* url) { + cc_uint64 beg, end; + int elapsed; + + beg = Stopwatch_Measure(); + req->result = HttpBackend_Do(req, url); + end = Stopwatch_Measure(); + + elapsed = Stopwatch_ElapsedMS(beg, end); + Platform_Log4("HTTP: result %e (http %i) in %i ms (%i bytes)", + &req->result, &req->statusCode, &elapsed, &req->size); + + Http_FinishRequest(req); +} + +static void ClearCurrentRequest(void) { + Mutex_Lock(curRequestMutex); + { + http_curRequest.id = 0; + http_curRequest.progress = HTTP_PROGRESS_NOT_WORKING_ON; + } + Mutex_Unlock(curRequestMutex); +} + +static void DoRequest(struct HttpRequest* request) { + char urlBuffer[URL_MAX_SIZE]; cc_string url; + + String_InitArray(url, urlBuffer); + PrepareCurrentRequest(request, &url); + PerformRequest(&http_curRequest, &url); + ClearCurrentRequest(); +} + +static void WorkerLoop(void) { + struct HttpRequest request; + cc_bool hasRequest; + + for (;;) { + hasRequest = false; + + Mutex_Lock(pendingMutex); + { + if (pendingReqs.count) { + HttpRequest_Copy(&request, &pendingReqs.entries[0]); + hasRequest = true; + RequestList_RemoveAt(&pendingReqs, 0); + } + } + Mutex_Unlock(pendingMutex); + + if (hasRequest) { + DoRequest(&request); + } else { + /* Block until another thread submits a request to do */ + Platform_LogConst("Going back to sleep..."); + Waitable_Wait(workerWaitable); + } + } +} + +/* Adds a req to the list of pending requests, waking up worker thread if needed */ +static void HttpBackend_Add(struct HttpRequest* req, cc_uint8 flags) { +#if defined CC_BUILD_PSP || defined CC_BUILD_NDS + /* TODO why doesn't threading work properly on PSP */ + DoRequest(req); +#else + Mutex_Lock(pendingMutex); + { + RequestList_Append(&pendingReqs, req, flags); + } + Mutex_Unlock(pendingMutex); + Waitable_Signal(workerWaitable); +#endif +} + +/*########################################################################################################################* +*-----------------------------------------------------Http component------------------------------------------------------* +*#########################################################################################################################*/ +static void Http_Init(void) { + Http_InitCommon(); + http_curRequest.progress = HTTP_PROGRESS_NOT_WORKING_ON; + /* Http component gets initialised multiple times on Android */ + if (workerThread) return; + + HttpBackend_Init(); + RequestList_Init(&pendingReqs); + RequestList_Init(&processedReqs); + + workerWaitable = Waitable_Create("HTTP wakeup"); + pendingMutex = Mutex_Create("HTTP pending"); + processedMutex = Mutex_Create("HTTP processed"); + curRequestMutex = Mutex_Create("HTTP current"); + + Thread_Run(&workerThread, WorkerLoop, 128 * 1024, "HTTP"); +} +#endif diff --git a/src/Input.c b/src/Input.c new file mode 100644 index 0000000..0f6bc80 --- /dev/null +++ b/src/Input.c @@ -0,0 +1,1467 @@ +#include "Input.h" +#include "String.h" +#include "Event.h" +#include "Funcs.h" +#include "Options.h" +#include "Logger.h" +#include "Platform.h" +#include "Chat.h" +#include "Utils.h" +#include "Server.h" +#include "HeldBlockRenderer.h" +#include "Game.h" +#include "ExtMath.h" +#include "Camera.h" +#include "Inventory.h" +#include "World.h" +#include "Event.h" +#include "Window.h" +#include "Entity.h" +#include "Screens.h" +#include "Block.h" +#include "Menus.h" +#include "Gui.h" +#include "Protocol.h" +#include "AxisLinesRenderer.h" +#include "Picking.h" + +struct _InputState Input; +static cc_bool input_buttonsDown[3]; +static int input_pickingId = -1; +static double input_lastClick; +static float input_fovIndex = -1.0f; +#ifdef CC_BUILD_WEB +static cc_bool suppressEscape; +#endif +enum MouseButton_ { MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE }; +/* Raises PointerEvents.Up or PointerEvents.Down */ +static void Pointer_SetPressed(int idx, cc_bool pressed); + + +/*########################################################################################################################* +*------------------------------------------------------Touch support------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_TOUCH +static struct TouchPointer { + long id; + cc_uint8 type; + int begX, begY; + double start; +} touches[INPUT_MAX_POINTERS]; + +int Pointers_Count; +int Input_TapMode = INPUT_MODE_PLACE; +int Input_HoldMode = INPUT_MODE_DELETE; +cc_bool Input_TouchMode; + +static void MouseStatePress(int button); +static void MouseStateRelease(int button); + +static cc_bool AnyBlockTouches(void) { + int i; + for (i = 0; i < Pointers_Count; i++) { + if (!(touches[i].type & TOUCH_TYPE_BLOCKS)) continue; + + /* Touch might be an 'all' type - remove 'gui' type */ + touches[i].type &= TOUCH_TYPE_BLOCKS | TOUCH_TYPE_CAMERA; + return true; + } + return false; +} + +static void ClearTouches(void) { + int i; + for (i = 0; i < INPUT_MAX_POINTERS; i++) touches[i].type = 0; + Pointers_Count = Input_TouchMode ? 0 : 1; +} + +void Input_SetTouchMode(cc_bool enabled) { + Input_TouchMode = enabled; + ClearTouches(); +} + +static cc_bool MovedFromBeg(int i, int x, int y) { + return Math_AbsI(x - touches[i].begX) > Display_ScaleX(5) || + Math_AbsI(y - touches[i].begY) > Display_ScaleY(5); +} + +static cc_bool TryUpdateTouch(long id, int x, int y) { + int i; + for (i = 0; i < Pointers_Count; i++) { + if (touches[i].id != id || !touches[i].type) continue; + + if (Input.RawMode && (touches[i].type & TOUCH_TYPE_CAMERA)) { + /* If the pointer hasn't been locked to gui or block yet, moving a bit */ + /* should cause the pointer to get locked to camera movement. */ + if (touches[i].type == TOUCH_TYPE_ALL && MovedFromBeg(i, x, y)) { + /* Allow a little bit of leeway because though, because devices */ + /* might still report a few pixels of movement depending on how */ + /* user is holding the finger down on the touch surface */ + if (touches[i].type == TOUCH_TYPE_ALL) touches[i].type = TOUCH_TYPE_CAMERA; + } + Event_RaiseRawMove(&PointerEvents.RawMoved, x - Pointers[i].x, y - Pointers[i].y); + } + Pointer_SetPosition(i, x, y); + return true; + } + return false; +} + +void Input_AddTouch(long id, int x, int y) { + int i; + /* Check if already existing pointer with same ID */ + if (TryUpdateTouch(id, x, y)) return; + + for (i = 0; i < INPUT_MAX_POINTERS; i++) { + if (touches[i].type) continue; + + touches[i].id = id; + touches[i].type = TOUCH_TYPE_ALL; + touches[i].begX = x; + touches[i].begY = y; + + touches[i].start = Game.Time; + /* Also set last click time, otherwise quickly tapping */ + /* sometimes triggers a 'delete' in InputHandler_Tick, */ + /* and then another 'delete' in CheckBlockTap. */ + input_lastClick = Game.Time; + + if (i == Pointers_Count) Pointers_Count++; + Pointer_SetPosition(i, x, y); + Pointer_SetPressed(i, true); + return; + } +} +void Input_UpdateTouch(long id, int x, int y) { TryUpdateTouch(id, x, y); } + +/* Quickly tapping should trigger a block place/delete */ +static void CheckBlockTap(int i) { + int btn, pressed; + if (Game.Time > touches[i].start + 0.25) return; + if (touches[i].type != TOUCH_TYPE_ALL) return; + + if (Input_TapMode == INPUT_MODE_PLACE) { + btn = MOUSE_RIGHT; + } else if (Input_TapMode == INPUT_MODE_DELETE) { + btn = MOUSE_LEFT; + } else { return; } + + pressed = input_buttonsDown[btn]; + MouseStatePress(btn); + + if (btn == MOUSE_LEFT) { + InputHandler_DeleteBlock(); + } else { + InputHandler_PlaceBlock(); + } + if (!pressed) MouseStateRelease(btn); +} + +void Input_RemoveTouch(long id, int x, int y) { + int i; + for (i = 0; i < Pointers_Count; i++) { + if (touches[i].id != id || !touches[i].type) continue; + + Pointer_SetPosition(i, x, y); + Pointer_SetPressed(i, false); + + /* found the touch, remove it */ + Pointer_SetPosition(i, -100000, -100000); + touches[i].type = 0; + + if ((i + 1) == Pointers_Count) Pointers_Count--; + return; + } +} +#else +static void ClearTouches(void) { } +#endif + + +/*########################################################################################################################* +*-----------------------------------------------------------Key-----------------------------------------------------------* +*#########################################################################################################################*/ +#define Key_Function_Names \ +"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10",\ +"F11", "F12", "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20",\ +"F21", "F22", "F23", "F24" +#define Key_Ascii_Names \ +"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",\ +"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",\ +"U", "V", "W", "X", "Y", "Z" +#define Pad_Names \ +"PAD_A", "PAD_B", "PAD_X", "PAD_Y", "PAD_L", "PAD_R", \ +"PAD_Z", "PAD_C", "PAD_D", \ +"PAD_LEFT", "PAD_RIGHT", "PAD_UP", "PAD_DOWN", \ +"PAD_START", "PAD_SELECT", "PAD_ZL", "PAD_ZR", \ +"PAD_LSTICK", "PAD_RSTICK", \ +"PAD_CLEFT", "PAD_CRIGHT", "PAD_CUP", "PAD_CDOWN" + +/* Names for each input button when stored to disc */ +static const char* const storageNames[INPUT_COUNT] = { + "None", + Key_Function_Names, + "Tilde", "Minus", "Plus", "BracketLeft", "BracketRight", "Slash", + "Semicolon", "Quote", "Comma", "Period", "BackSlash", + "ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", + "AltLeft", "AltRight", "WinLeft", "WinRight", + "Up", "Down", "Left", "Right", + "Number0", "Number1", "Number2", "Number3", "Number4", + "Number5", "Number6", "Number7", "Number8", "Number9", + "Insert", "Delete", "Home", "End", "PageUp", "PageDown", + "Menu", + Key_Ascii_Names, + "Enter", "Escape", "Space", "BackSpace", "Tab", "CapsLock", + "ScrollLock", "PrintScreen", "Pause", "NumLock", + "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", + "Keypad5", "Keypad6", "Keypad7", "Keypad8", "Keypad9", + "KeypadDivide", "KeypadMultiply", "KeypadSubtract", + "KeypadAdd", "KeypadDecimal", "KeypadEnter", + + "XButton1", "XButton2", "LeftMouse", "RightMouse", "MiddleMouse", + "WheelUp", "WheelDown", "WheelLeft", "WheelRight", + "XButton3", "XButton4", "XButton5", "XButton6", + + "VolumeMute", "VolumeUp", "VolumeDown", "Sleep", + "MediaNext", "MediaPrev", "MediaPlay", "MediaStop", + "BrowserPrev", "BrowserNext", "BrowserRefresh", "BrowserStop", "BrowserSsearch", "BrowserFavorites", "BrowserHome", + "LaunchMail", "LaunchMedia", "LaunchApp1", "LaunchCalc", + + Pad_Names +}; + +const char* const Input_DisplayNames[INPUT_COUNT] = { + "NONE", + Key_Function_Names, + "GRAVE", "MINUS", "PLUS", "LBRACKET", "RBRACKET", "SLASH", + "SEMICOLON", "APOSTROPHE", "COMMA", "PERIOD", "BACKSLASH", + "LSHIFT", "RSHIFT", "LCONTROL", "RCONTROL", + "LALT", "RALT", "LWIN", "RWIN", + "UP", "DOWN", "LEFT", "RIGHT", + "0", "1", "2", "3", "4", + "5", "6", "7", "8", "9", + "INSERT", "DELETE", "HOME", "END", "PRIOR", "DOWN", + "MENU", + Key_Ascii_Names, + "RETURN", "ESCAPE", "SPACE", "BACK", "TAB", "CAPITAL", + "SCROLL", "PRINT", "PAUSE", "NUMLOCK", + "NUMPAD0", "NUMPAD1", "NUMPAD2", "NUMPAD3", "NUMPAD4", + "NUMPAD5", "NUMPAD6", "NUMPAD7", "NUMPAD8", "NUMPAD9", + "DIVIDE", "MULTIPLY", "SUBTRACT", + "ADD", "DECIMAL", "NUMPADENTER", + + "XBUTTON1", "XBUTTON2", "LMOUSE", "RMOUSE", "MMOUSE", + "WHEELUP", "WHEELDOWN", "WHEELLEFT", "WHEELRIGHT", + "XBUTTON3", "XBUTTON4", "XBUTTON5", "XBUTTON6", + + "VOLUMEMUTE", "VOLUMEUP", "VOLUMEDOWN", "SLEEP", + "MEDIANEXT", "MEDIAPREV", "MEDIAPLAY", "MEDIASTOP", + "BROWSERPREV", "BROWSERNEXT", "BROWSERREFRESH", "BROWSERSTOP", "BROWSERSEARCH", "BROWSERFAVORITES", "BROWSERHOME", + "LAUNCHMAIL", "LAUNCHMEDIA", "LAUNCHAPP1", "LAUNCHCALC", + + Pad_Names +}; + +void Input_SetPressed(int key) { + cc_bool wasPressed = Input.Pressed[key]; + Input.Pressed[key] = true; + Event_RaiseInput(&InputEvents.Down, key, wasPressed); + + if (key == 'C' && Input_IsActionPressed()) Event_RaiseInput(&InputEvents.Down, INPUT_CLIPBOARD_COPY, 0); + if (key == 'V' && Input_IsActionPressed()) Event_RaiseInput(&InputEvents.Down, INPUT_CLIPBOARD_PASTE, 0); + + /* don't allow multiple left mouse down events */ + if (key != CCMOUSE_L || wasPressed) return; + Pointer_SetPressed(0, true); +} + +void Input_SetReleased(int key) { + if (!Input.Pressed[key]) return; + Input.Pressed[key] = false; + + Event_RaiseInt(&InputEvents.Up, key); + if (key == CCMOUSE_L) Pointer_SetPressed(0, false); +} + +void Input_Set(int key, int pressed) { + if (pressed) { + Input_SetPressed(key); + } else { + Input_SetReleased(key); + } +} + +void Input_SetNonRepeatable(int key, int pressed) { + if (pressed) { + if (Input.Pressed[key]) return; + Input_SetPressed(key); + } else { + Input_SetReleased(key); + } +} + +void Input_Clear(void) { + int i; + for (i = 0; i < INPUT_COUNT; i++) + { + if (Input.Pressed[i]) Input_SetReleased(i); + } + /* TODO: Properly release instead of just clearing */ + ClearTouches(); +} + +int Input_CalcDelta(int key, int horDelta, int verDelta) { + if (Input_IsLeftButton(key) || key == CCKEY_KP4) return -horDelta; + if (Input_IsRightButton(key) || key == CCKEY_KP6) return +horDelta; + if (Input_IsUpButton(key) || key == CCKEY_KP8) return -verDelta; + if (Input_IsDownButton(key) || key == CCKEY_KP2) return +verDelta; + + return 0; +} + + +/*########################################################################################################################* +*----------------------------------------------------------Mouse----------------------------------------------------------* +*#########################################################################################################################*/ +struct Pointer Pointers[INPUT_MAX_POINTERS]; + +void Pointer_SetPressed(int idx, cc_bool pressed) { + if (pressed) { + Event_RaiseInt(&PointerEvents.Down, idx); + } else { + Event_RaiseInt(&PointerEvents.Up, idx); + } +} + +static float scrollingVAcc; +void Mouse_ScrollVWheel(float delta) { + int steps = Utils_AccumulateWheelDelta(&scrollingVAcc, delta); + Event_RaiseFloat(&InputEvents.Wheel, delta); + + if (steps > 0) { + for (; steps != 0; steps--) + Input_SetPressed(CCWHEEL_UP); + Input_SetReleased(CCWHEEL_UP); + } else if (steps < 0) { + for (; steps != 0; steps++) + Input_SetPressed(CCWHEEL_DOWN); + Input_SetReleased(CCWHEEL_DOWN); + } +} + +static float scrollingHAcc; +void Mouse_ScrollHWheel(float delta) { + int steps = Utils_AccumulateWheelDelta(&scrollingHAcc, delta); + + if (steps > 0) { + for (; steps != 0; steps--) + Input_SetPressed(CCWHEEL_RIGHT); + Input_SetReleased(CCWHEEL_RIGHT); + } else if (steps < 0) { + for (; steps != 0; steps++) + Input_SetPressed(CCWHEEL_LEFT); + Input_SetReleased(CCWHEEL_LEFT); + } +} + +void Pointer_SetPosition(int idx, int x, int y) { + if (x == Pointers[idx].x && y == Pointers[idx].y) return; + /* TODO: reset to -1, -1 when pointer is removed */ + Pointers[idx].x = x; Pointers[idx].y = y; + +#ifdef CC_BUILD_TOUCH + if (Input_TouchMode && !(touches[idx].type & TOUCH_TYPE_GUI)) return; +#endif + Event_RaiseInt(&PointerEvents.Moved, idx); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Keybinds--------------------------------------------------------* +*#########################################################################################################################*/ +BindMapping PadBind_Mappings[BIND_COUNT]; +BindMapping KeyBind_Mappings[BIND_COUNT]; +BindTriggered Bind_OnTriggered[BIND_COUNT]; +BindReleased Bind_OnReleased[BIND_COUNT]; + +const BindMapping PadBind_Defaults[BIND_COUNT] = { + { CCPAD_UP, 0 }, { CCPAD_DOWN, 0 }, /* BIND_FORWARD, BIND_BACK */ + { CCPAD_LEFT, 0 }, { CCPAD_RIGHT, 0 }, /* BIND_LEFT, BIND_RIGHT */ + { CCPAD_A, 0 }, { 0, 0 }, /* BIND_JUMP, BIND_RESPAWN */ + { CCPAD_START, 0 }, { CCPAD_Y, 0 }, /* BIND_SET_SPAWN, BIND_CHAT */ + { CCPAD_X, 0 }, { 0, 0 }, /* BIND_INVENTORY, BIND_FOG */ + { CCPAD_START, 0 }, { 0, 0 }, /* BIND_SEND_CHAT, BIND_TABLIST */ + { CCPAD_B, CCPAD_L},{ CCPAD_B, CCPAD_X},/* BIND_SPEED, BIND_NOCLIP */ + { CCPAD_B, CCPAD_R }, /* BIND_FLY */ + {CCPAD_B,CCPAD_UP},{CCPAD_B,CCPAD_DOWN},/* BIND_FLY_UP, BIND_FLY_DOWN */ + { 0, 0 }, { 0, 0 }, /* BIND_EXT_INPUT, BIND_HIDE_FPS */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_SCREENSHOT, BIND_FULLSCREEN, BIND_THIRD_PERSON, BIND_HIDE_GUI */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_AXIS_LINES, BIND_ZOOM_SCROLL, BIND_HALF_SPEED */ + { CCPAD_L, 0 }, { 0, 0 },{ CCPAD_R, 0 },/* BIND_DELETE_BLOCK, BIND_PICK_BLOCK, BIND_PLACE_BLOCK */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_AUTOROTATE, BIND_HOTBAR_SWITCH, BIND_SMOOTH_CAMERA */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_DROP_BLOCK, BIND_IDOVERLAY, BIND_BREAK_LIQUIDS */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_LOOK_UP, BIND_LOOK_DOWN, BIND_LOOK_RIGHT, BIND_LOOK_LEFT */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_HOTBAR_1, BIND_HOTBAR_2, BIND_HOTBAR_3 */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_HOTBAR_4, BIND_HOTBAR_5, BIND_HOTBAR_6 */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_HOTBAR_7, BIND_HOTBAR_8, BIND_HOTBAR_9 */ + { CCPAD_ZL, 0 }, { CCPAD_ZR, 0 } /* BIND_HOTBAR_LEFT, BIND_HOTBAR_RIGHT */ +}; + +const BindMapping KeyBind_Defaults[BIND_COUNT] = { + { 'W', 0 }, { 'S', 0 }, { 'A', 0 }, { 'D', 0 }, /* BIND_FORWARD - BIND_RIGHT */ + { CCKEY_SPACE, 0 }, { 'R', 0 }, /* BIND_JUMP, BIND_RESPAWN */ + { CCKEY_ENTER, 0 }, { 'T', 0 }, /* BIND_SET_SPAWN, BIND_CHAT */ + { 'B', 0 }, { 'F', 0 }, /* BIND_INVENTORY, BIND_FOG */ + { CCKEY_ENTER, 0 }, { CCKEY_TAB, 0 }, /* BIND_SEND_CHAT, BIND_TABLIST */ + { CCKEY_LSHIFT, 0 }, { 'X', 0}, { 'Z', 0 }, /* BIND_SPEED, BIND_NOCLIP, BIND_FLY */ + { 'Q', 0 }, { 'E', 0 }, /* BIND_FLY_UP, BIND_FLY_DOWN */ + { CCKEY_LALT, 0 }, { CCKEY_F3, 0 }, /* BIND_EXT_INPUT, BIND_HIDE_FPS */ + { CCKEY_F12, 0 }, { CCKEY_F11, 0 }, /* BIND_SCREENSHOT, BIND_FULLSCREEN */ + { CCKEY_F5, 0 }, { CCKEY_F1, 0 }, /* BIND_THIRD_PERSON, BIND_HIDE_GUI */ + { CCKEY_F7, 0 }, { 'C', 0 }, { CCKEY_LCTRL, 0 },/* BIND_AXIS_LINES, BIND_ZOOM_SCROLL, BIND_HALF_SPEED */ + { CCMOUSE_L, 0},{ CCMOUSE_M, 0},{ CCMOUSE_R, 0},/* BIND_DELETE_BLOCK, BIND_PICK_BLOCK, BIND_PLACE_BLOCK */ + { CCKEY_F6, 0 }, { CCKEY_LALT, 0 }, /* BIND_AUTOROTATE, BIND_HOTBAR_SWITCH */ + { CCKEY_F8, 0 }, { 'G', 0 }, /* BIND_SMOOTH_CAMERA, BIND_DROP_BLOCK */ + { CCKEY_F10, 0 }, { 0, 0 }, /* BIND_IDOVERLAY, BIND_BREAK_LIQUIDS */ + { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, /* BIND_LOOK_UP, BIND_LOOK_DOWN, BIND_LOOK_RIGHT, BIND_LOOK_LEFT */ + { '1', 0 }, { '2', 0 }, { '3', 0 }, /* BIND_HOTBAR_1, BIND_HOTBAR_2, BIND_HOTBAR_3 */ + { '4', 0 }, { '5', 0 }, { '6', 0 }, /* BIND_HOTBAR_4, BIND_HOTBAR_5, BIND_HOTBAR_6 */ + { '7', 0 }, { '8', 0 }, { '9', 0 }, /* BIND_HOTBAR_7, BIND_HOTBAR_8, BIND_HOTBAR_9 */ + { 0, 0 }, { 0, 0 } /* BIND_HOTBAR_LEFT, BIND_HOTBAR_RIGHT */ +}; + +static const char* const bindNames[BIND_COUNT] = { + "Forward", "Back", "Left", "Right", + "Jump", "Respawn", "SetSpawn", "Chat", "Inventory", + "ToggleFog", "SendChat", "PlayerList", + "Speed", "NoClip", "Fly", "FlyUp", "FlyDown", + "ExtInput", "HideFPS", "Screenshot", "Fullscreen", + "ThirdPerson", "HideGUI", "AxisLines", "ZoomScrolling", + "HalfSpeed", "DeleteBlock", "PickBlock", "PlaceBlock", + "AutoRotate", "HotbarSwitching", "SmoothCamera", + "DropBlock", "IDOverlay", "BreakableLiquids", + "LookUp", "LookDown", "LookRight", "LookLeft", + "Hotbar1", "Hotbar2", "Hotbar3", + "Hotbar4", "Hotbar5", "Horbar6", + "Hotbar7", "Hotbar8", "Hotbar9", + "HotbarLeft", "HotbarRight" +}; + + +#define BindMapping2_Claims(mapping, btn) (Input.Pressed[(mapping)->button1] && (mapping)->button2 == btn) +static cc_bool Mappings_DoesClaim(InputBind binding, int btn, BindMapping* mappings) { + BindMapping* bind = &mappings[binding]; + int i; + if (bind->button2) return BindMapping2_Claims(bind, btn); + + /* Two button mapping takes priority over one button mapping */ + for (i = 0; i < BIND_COUNT; i++) + { + if (mappings[i].button2 && BindMapping2_Claims(&mappings[i], btn)) return false; + } + return bind->button1 == btn; +} + +static cc_bool Mappings_IsPressed(InputBind binding, BindMapping* mappings) { + BindMapping* bind = &mappings[binding]; + int btn = bind->button1; + int i; + + if (!Input.Pressed[btn]) return false; + if (bind->button2) return Input.Pressed[bind->button2]; + + /* Two button mappings to the button takes priority one button mapping */ + for (i = 0; i < BIND_COUNT; i++) + { + bind = &mappings[i]; + if (!bind->button2) continue; + if (!(bind->button1 == btn || bind->button2 == btn)) continue; + + if (Input.Pressed[bind->button1] && Input.Pressed[bind->button2]) return false; + } + return true; +} + + +cc_bool InputBind_Claims(InputBind binding, int btn) { + return Mappings_DoesClaim(binding, btn, KeyBind_Mappings) || + Mappings_DoesClaim(binding, btn, PadBind_Mappings); +} + +cc_bool InputBind_IsPressed(InputBind binding) { + return Mappings_IsPressed(binding, KeyBind_Mappings) || + Mappings_IsPressed(binding, PadBind_Mappings); +} + +static void KeyBind_Load(const char* prefix, BindMapping* keybinds, const BindMapping* defaults) { + cc_string name; char nameBuffer[STRING_SIZE + 1]; + BindMapping mapping; + cc_string str, part1, part2; + int i; + + String_InitArray_NT(name, nameBuffer); + for (i = 0; i < BIND_COUNT; i++) + { + name.length = 0; + String_Format1(&name, prefix, bindNames[i]); + name.buffer[name.length] = '\0'; + + if (!Options_UNSAFE_Get(name.buffer, &str)) { + keybinds[i] = defaults[i]; + continue; + } + + String_UNSAFE_Separate(&str, ',', &part1, &part2); + mapping.button1 = Utils_ParseEnum(&part1, defaults[i].button1, storageNames, INPUT_COUNT); + mapping.button2 = Utils_ParseEnum(&part2, defaults[i].button2, storageNames, INPUT_COUNT); + + if (mapping.button1 == CCKEY_ESCAPE) mapping = defaults[i]; + keybinds[i] = mapping; + } +} + +static void InputBind_Set(InputBind binding, int btn, BindMapping* binds, const char* fmt) { + cc_string name; char nameBuffer[STRING_SIZE]; + cc_string value; + String_InitArray(name, nameBuffer); + + String_Format1(&name, fmt, bindNames[binding]); + value = String_FromReadonly(storageNames[btn]); + Options_SetString(&name, &value); + + BindMapping_Set(&binds[binding], btn, 0); +} + +void KeyBind_Set(InputBind binding, int btn) { + InputBind_Set(binding, btn, KeyBind_Mappings, "key-%c"); +} + +void PadBind_Set(InputBind binding, int btn) { + InputBind_Set(binding, btn, PadBind_Mappings, "pad-%c"); +} + +static void InputBind_ResetOption(InputBind binding, const char* fmt) { + cc_string name; char nameBuffer[STRING_SIZE]; + String_InitArray(name, nameBuffer); + + String_Format1(&name, fmt, bindNames[binding]); + Options_SetString(&name, &String_Empty); +} + +void KeyBind_Reset(InputBind binding) { + InputBind_ResetOption(binding, "key-%c"); + KeyBind_Mappings[binding] = KeyBind_Defaults[binding]; +} + +void PadBind_Reset(InputBind binding) { + InputBind_ResetOption(binding, "pad-%c"); + PadBind_Mappings[binding] = PadBind_Defaults[binding]; +} + +/* Initialises and loads input bindings from options */ +static void KeyBind_Init(void) { + KeyBind_Load("key-%c", KeyBind_Mappings, KeyBind_Defaults); + KeyBind_Load("pad-%c", PadBind_Mappings, PadBind_Defaults); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Gamepad---------------------------------------------------------* +*#########################################################################################################################*/ +#define GAMEPAD_BEG_BTN CCPAD_A +#define GAMEPAD_BTN_COUNT (INPUT_COUNT - GAMEPAD_BEG_BTN) + +int Gamepad_AxisBehaviour[2] = { AXIS_BEHAVIOUR_MOVEMENT, AXIS_BEHAVIOUR_CAMERA }; +int Gamepad_AxisSensitivity[2] = { AXIS_SENSI_NORMAL, AXIS_SENSI_NORMAL }; +static const float axis_sensiFactor[] = { 0.25f, 0.5f, 1.0f, 2.0f, 4.0f }; + +struct GamepadState { + float axisX[2], axisY[2]; + cc_bool pressed[GAMEPAD_BTN_COUNT]; + float holdtime[GAMEPAD_BTN_COUNT]; +}; +static struct GamepadState gamepads[INPUT_MAX_GAMEPADS]; + +static void Gamepad_Update(struct GamepadState* pad, float delta) { + int btn; + for (btn = 0; btn < GAMEPAD_BTN_COUNT; btn++) + { + if (!pad->pressed[btn]) continue; + pad->holdtime[btn] += delta; + if (pad->holdtime[btn] < 1.0f) continue; + + /* Held for over a second, trigger a fake press */ + pad->holdtime[btn] = 0; + Input_SetPressed(btn + GAMEPAD_BEG_BTN); + } +} + + +void Gamepad_SetButton(int port, int btn, int pressed) { + struct GamepadState* pad = &gamepads[port]; + int i; + btn -= GAMEPAD_BEG_BTN; + + /* Reset hold tracking time */ + if (pressed && !pad->pressed[btn]) pad->holdtime[btn] = 0; + pad->pressed[btn] = pressed != 0;; + + /* Set pressed if button pressed on any gamepad, to avoid constant flip flopping */ + /* between pressed and non-pressed when multiple controllers are plugged in */ + for (i = 0; i < INPUT_MAX_GAMEPADS; i++) + pressed |= gamepads[i].pressed[btn]; + + Input_SetNonRepeatable(btn + GAMEPAD_BEG_BTN, pressed); +} + +void Gamepad_SetAxis(int port, int axis, float x, float y, float delta) { + gamepads[port].axisX[axis] = x; + gamepads[port].axisY[axis] = y; + if (x == 0 && y == 0) return; + + int sensi = Gamepad_AxisSensitivity[axis]; + float scale = delta * 60.0f * axis_sensiFactor[sensi]; + Event_RaisePadAxis(&ControllerEvents.AxisUpdate, port, axis, x * scale, y * scale); +} + +void Gamepad_Tick(float delta) { + int port; + Window_ProcessGamepads(delta); + + for (port = 0; port < INPUT_MAX_GAMEPADS; port++) + { + Gamepad_Update(&gamepads[port], delta); + } +} + +static void PlayerInputPad(int port, int axis, struct LocalPlayer* p, float* xMoving, float* zMoving) { + float x, y, angle; + if (Gamepad_AxisBehaviour[axis] != AXIS_BEHAVIOUR_MOVEMENT) return; + + x = gamepads[port].axisX[axis]; + y = gamepads[port].axisY[axis]; + + if (x != 0 || y != 0) { + angle = Math_Atan2f(x, y); + *xMoving = Math_CosF(angle); + *zMoving = Math_SinF(angle); + } +} + +static void PlayerInputGamepad(struct LocalPlayer* p, float* xMoving, float* zMoving) { + int port; + for (port = 0; port < INPUT_MAX_GAMEPADS; port++) + { + /* In splitscreen mode, tie a controller to a specific player*/ + if (Game_NumLocalPlayers > 1 && p->index != port) continue; + + PlayerInputPad(port, PAD_AXIS_LEFT, p, xMoving, zMoving); + PlayerInputPad(port, PAD_AXIS_RIGHT, p, xMoving, zMoving); + } +} +static struct LocalPlayerInput gamepadInput = { PlayerInputGamepad }; + + +/*########################################################################################################################* +*---------------------------------------------------------Hotkeys---------------------------------------------------------* +*#########################################################################################################################*/ +const cc_uint8 Hotkeys_LWJGL[256] = { + 0, CCKEY_ESCAPE, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', CCKEY_MINUS, CCKEY_EQUALS, CCKEY_BACKSPACE, CCKEY_TAB, + 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', CCKEY_LBRACKET, CCKEY_RBRACKET, CCKEY_ENTER, CCKEY_LCTRL, 'A', 'S', + 'D', 'F', 'G', 'H', 'J', 'K', 'L', CCKEY_SEMICOLON, CCKEY_QUOTE, CCKEY_TILDE, CCKEY_LSHIFT, CCKEY_BACKSLASH, 'Z', 'X', 'C', 'V', + 'B', 'N', 'M', CCKEY_COMMA, CCKEY_PERIOD, CCKEY_SLASH, CCKEY_RSHIFT, 0, CCKEY_LALT, CCKEY_SPACE, CCKEY_CAPSLOCK, CCKEY_F1, CCKEY_F2, CCKEY_F3, CCKEY_F4, CCKEY_F5, + CCKEY_F6, CCKEY_F7, CCKEY_F8, CCKEY_F9, CCKEY_F10, CCKEY_NUMLOCK, CCKEY_SCROLLLOCK, CCKEY_KP7, CCKEY_KP8, CCKEY_KP9, CCKEY_KP_MINUS, CCKEY_KP4, CCKEY_KP5, CCKEY_KP6, CCKEY_KP_PLUS, CCKEY_KP1, + CCKEY_KP2, CCKEY_KP3, CCKEY_KP0, CCKEY_KP_DECIMAL, 0, 0, 0, CCKEY_F11, CCKEY_F12, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, CCKEY_F13, CCKEY_F14, CCKEY_F15, CCKEY_F16, CCKEY_F17, CCKEY_F18, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CCKEY_KP_PLUS, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CCKEY_KP_ENTER, CCKEY_RCTRL, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, CCKEY_KP_DIVIDE, 0, 0, CCKEY_RALT, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, CCKEY_PAUSE, 0, CCKEY_HOME, CCKEY_UP, CCKEY_PAGEUP, 0, CCKEY_LEFT, 0, CCKEY_RIGHT, 0, CCKEY_END, + CCKEY_DOWN, CCKEY_PAGEDOWN, CCKEY_INSERT, CCKEY_DELETE, 0, 0, 0, 0, 0, 0, 0, CCKEY_LWIN, CCKEY_RWIN, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; +struct HotkeyData HotkeysList[HOTKEYS_MAX_COUNT]; +struct StringsBuffer HotkeysText; + +static void Hotkeys_QuickSort(int left, int right) { + struct HotkeyData* keys = HotkeysList; struct HotkeyData key; + + while (left < right) { + int i = left, j = right; + cc_uint8 pivot = keys[(i + j) >> 1].mods; + + /* partition the list */ + while (i <= j) { + while (pivot < keys[i].mods) i++; + while (pivot > keys[j].mods) j--; + QuickSort_Swap_Maybe(); + } + /* recurse into the smaller subset */ + QuickSort_Recurse(Hotkeys_QuickSort) + } +} + +static void Hotkeys_AddNewHotkey(int trigger, cc_uint8 modifiers, const cc_string* text, cc_uint8 flags) { + struct HotkeyData hKey; + hKey.trigger = trigger; + hKey.mods = modifiers; + hKey.textIndex = HotkeysText.count; + hKey.flags = flags; + + if (HotkeysText.count == HOTKEYS_MAX_COUNT) { + Chat_AddRaw("&cCannot define more than 256 hotkeys"); + return; + } + + HotkeysList[HotkeysText.count] = hKey; + StringsBuffer_Add(&HotkeysText, text); + /* sort so that hotkeys with largest modifiers are first */ + Hotkeys_QuickSort(0, HotkeysText.count - 1); +} + +static void Hotkeys_RemoveText(int index) { + struct HotkeyData* hKey = HotkeysList; + int i; + + for (i = 0; i < HotkeysText.count; i++, hKey++) { + if (hKey->textIndex >= index) hKey->textIndex--; + } + StringsBuffer_Remove(&HotkeysText, index); +} + + +void Hotkeys_Add(int trigger, cc_uint8 modifiers, const cc_string* text, cc_uint8 flags) { + struct HotkeyData* hk = HotkeysList; + int i; + + for (i = 0; i < HotkeysText.count; i++, hk++) { + if (hk->trigger != trigger || hk->mods != modifiers) continue; + Hotkeys_RemoveText(hk->textIndex); + + hk->flags = flags; + hk->textIndex = HotkeysText.count; + StringsBuffer_Add(&HotkeysText, text); + return; + } + Hotkeys_AddNewHotkey(trigger, modifiers, text, flags); +} + +cc_bool Hotkeys_Remove(int trigger, cc_uint8 modifiers) { + struct HotkeyData* hk = HotkeysList; + int i, j; + + for (i = 0; i < HotkeysText.count; i++, hk++) { + if (hk->trigger != trigger || hk->mods != modifiers) continue; + Hotkeys_RemoveText(hk->textIndex); + + for (j = i; j < HotkeysText.count; j++) { + HotkeysList[j] = HotkeysList[j + 1]; + } + return true; + } + return false; +} + +int Hotkeys_FindPartial(int key) { + struct HotkeyData hk; + int i, modifiers = 0; + + if (Input_IsCtrlPressed()) modifiers |= HOTKEY_MOD_CTRL; + if (Input_IsShiftPressed()) modifiers |= HOTKEY_MOD_SHIFT; + if (Input_IsAltPressed()) modifiers |= HOTKEY_MOD_ALT; + + for (i = 0; i < HotkeysText.count; i++) { + hk = HotkeysList[i]; + /* e.g. if holding Ctrl and Shift, a hotkey with only Ctrl modifiers matches */ + if ((hk.mods & modifiers) == hk.mods && hk.trigger == key) return i; + } + return -1; +} + +static const cc_string prefix = String_FromConst("hotkey-"); +static void StoredHotkey_Parse(cc_string* key, cc_string* value) { + cc_string strKey, strMods, strMore, strText; + int trigger; + cc_uint8 modifiers; + cc_bool more; + + /* Format is: key&modifiers = more-input&text */ + key->length -= prefix.length; key->buffer += prefix.length; + + if (!String_UNSAFE_Separate(key, '&', &strKey, &strMods)) return; + if (!String_UNSAFE_Separate(value, '&', &strMore, &strText)) return; + + trigger = Utils_ParseEnum(&strKey, INPUT_NONE, storageNames, INPUT_COUNT); + if (trigger == INPUT_NONE) return; + if (!Convert_ParseUInt8(&strMods, &modifiers)) return; + if (!Convert_ParseBool(&strMore, &more)) return; + + Hotkeys_Add(trigger, modifiers, &strText, more); +} + +static void StoredHotkeys_LoadAll(void) { + cc_string entry, key, value; + int i; + + for (i = 0; i < Options.count; i++) { + StringsBuffer_UNSAFE_GetRaw(&Options, i, &entry); + String_UNSAFE_Separate(&entry, '=', &key, &value); + + if (!String_CaselessStarts(&key, &prefix)) continue; + StoredHotkey_Parse(&key, &value); + } +} + +void StoredHotkeys_Load(int trigger, cc_uint8 modifiers) { + cc_string key, value; char keyBuffer[STRING_SIZE]; + String_InitArray(key, keyBuffer); + + String_Format2(&key, "hotkey-%c&%b", storageNames[trigger], &modifiers); + key.buffer[key.length] = '\0'; /* TODO: Avoid this null terminator */ + + Options_UNSAFE_Get(key.buffer, &value); + StoredHotkey_Parse(&key, &value); +} + +void StoredHotkeys_Remove(int trigger, cc_uint8 modifiers) { + cc_string key; char keyBuffer[STRING_SIZE]; + String_InitArray(key, keyBuffer); + + String_Format2(&key, "hotkey-%c&%b", storageNames[trigger], &modifiers); + Options_SetString(&key, NULL); +} + +void StoredHotkeys_Add(int trigger, cc_uint8 modifiers, cc_bool moreInput, const cc_string* text) { + cc_string key; char keyBuffer[STRING_SIZE]; + cc_string value; char valueBuffer[STRING_SIZE * 2]; + String_InitArray(key, keyBuffer); + String_InitArray(value, valueBuffer); + + String_Format2(&key, "hotkey-%c&%b", storageNames[trigger], &modifiers); + String_Format2(&value, "%t&%s", &moreInput, text); + Options_SetString(&key, &value); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Mouse helpers-------------------------------------------------------* +*#########################################################################################################################*/ +static void MouseStateUpdate(int button, cc_bool pressed) { + struct Entity* p; + input_buttonsDown[button] = pressed; + if (!Server.SupportsPlayerClick) return; + + /* defer getting the targeted entity, as it's a costly operation */ + if (input_pickingId == -1) { + p = &Entities.CurPlayer->Base; + input_pickingId = Entities_GetClosest(p); + + if (input_pickingId == -1) + input_pickingId = ENTITIES_SELF_ID; + } + + + CPE_SendPlayerClick(button, pressed, (EntityID)input_pickingId, &Game_SelectedPos); +} + +static void MouseStatePress(int button) { + input_lastClick = Game.Time; + input_pickingId = -1; + MouseStateUpdate(button, true); +} + +static void MouseStateRelease(int button) { + input_pickingId = -1; + if (!input_buttonsDown[button]) return; + MouseStateUpdate(button, false); +} + +void InputHandler_OnScreensChanged(void) { + input_lastClick = Game.Time; + input_pickingId = -1; + if (!Gui.InputGrab) return; + + /* If input is grabbed, then the mouse isn't used for picking blocks in world anymore. */ + /* So release all mouse buttons, since game stops sending PlayerClick during grabbed input */ + MouseStateRelease(MOUSE_LEFT); + MouseStateRelease(MOUSE_RIGHT); + MouseStateRelease(MOUSE_MIDDLE); +} + +static cc_bool TouchesSolid(BlockID b) { return Blocks.Collide[b] == COLLIDE_SOLID; } +static cc_bool PushbackPlace(struct AABB* blockBB) { + struct Entity* p = &Entities.CurPlayer->Base; + struct HacksComp* hacks = &Entities.CurPlayer->Hacks; + Face closestFace; + cc_bool insideMap; + + Vec3 pos = p->Position; + struct AABB playerBB; + struct LocationUpdate update; + + /* Offset position by the closest face */ + closestFace = Game_SelectedPos.closest; + if (closestFace == FACE_XMAX) { + pos.x = blockBB->Max.x + 0.5f; + } else if (closestFace == FACE_ZMAX) { + pos.z = blockBB->Max.z + 0.5f; + } else if (closestFace == FACE_XMIN) { + pos.x = blockBB->Min.x - 0.5f; + } else if (closestFace == FACE_ZMIN) { + pos.z = blockBB->Min.z - 0.5f; + } else if (closestFace == FACE_YMAX) { + pos.y = blockBB->Min.y + 1 + ENTITY_ADJUSTMENT; + } else if (closestFace == FACE_YMIN) { + pos.y = blockBB->Min.y - p->Size.y - ENTITY_ADJUSTMENT; + } + + /* Exclude exact map boundaries, otherwise player can get stuck outside map */ + /* Being vertically above the map is acceptable though */ + insideMap = + pos.x > 0.0f && pos.y >= 0.0f && pos.z > 0.0f && + pos.x < World.Width && pos.z < World.Length; + if (!insideMap) return false; + + AABB_Make(&playerBB, &pos, &p->Size); + if (!hacks->Noclip && Entity_TouchesAny(&playerBB, TouchesSolid)) { + /* Don't put player inside another block */ + return false; + } + + update.flags = LU_HAS_POS | LU_POS_ABSOLUTE_INSTANT; + update.pos = pos; + p->VTABLE->SetLocation(p, &update); + return true; +} + +static cc_bool IntersectsOthers(Vec3 pos, BlockID block) { + struct AABB blockBB, entityBB; + struct Entity* e; + int id; + + Vec3_Add(&blockBB.Min, &pos, &Blocks.MinBB[block]); + Vec3_Add(&blockBB.Max, &pos, &Blocks.MaxBB[block]); + + for (id = 0; id < ENTITIES_MAX_COUNT; id++) + { + e = Entities.List[id]; + if (!e || e == &Entities.CurPlayer->Base) continue; + + Entity_GetBounds(e, &entityBB); + entityBB.Min.y += 1.0f / 32.0f; /* when player is exactly standing on top of ground */ + if (AABB_Intersects(&entityBB, &blockBB)) return true; + } + return false; +} + +static cc_bool CheckIsFree(BlockID block) { + struct Entity* p = &Entities.CurPlayer->Base; + struct HacksComp* hacks = &Entities.CurPlayer->Hacks; + + Vec3 pos, nextPos; + struct AABB blockBB, playerBB; + struct LocationUpdate update; + + /* Non solid blocks (e.g. water/flowers) can always be placed on players */ + if (Blocks.Collide[block] != COLLIDE_SOLID) return true; + + IVec3_ToVec3(&pos, &Game_SelectedPos.translatedPos); + if (IntersectsOthers(pos, block)) return false; + + nextPos = p->next.pos; + Vec3_Add(&blockBB.Min, &pos, &Blocks.MinBB[block]); + Vec3_Add(&blockBB.Max, &pos, &Blocks.MaxBB[block]); + + /* NOTE: Need to also test against next position here, otherwise player can */ + /* fall through the block at feet as collision is performed against nextPos */ + Entity_GetBounds(p, &playerBB); + playerBB.Min.y = min(nextPos.y, playerBB.Min.y); + + if (hacks->Noclip || !AABB_Intersects(&playerBB, &blockBB)) return true; + if (hacks->CanPushbackBlocks && hacks->PushbackPlacing && hacks->Enabled) { + return PushbackPlace(&blockBB); + } + + playerBB.Min.y += 0.25f + ENTITY_ADJUSTMENT; + if (AABB_Intersects(&playerBB, &blockBB)) return false; + + /* Push player upwards when they are jumping and trying to place a block underneath them */ + nextPos.y = pos.y + Blocks.MaxBB[block].y + ENTITY_ADJUSTMENT; + + update.flags = LU_HAS_POS | LU_POS_ABSOLUTE_INSTANT; + update.pos = nextPos; + p->VTABLE->SetLocation(p, &update); + return true; +} + +void InputHandler_DeleteBlock(void) { + IVec3 pos; + BlockID old; + /* always play delete animations, even if we aren't deleting a block */ + HeldBlockRenderer_ClickAnim(true); + + pos = Game_SelectedPos.pos; + if (!Game_SelectedPos.valid || !World_Contains(pos.x, pos.y, pos.z)) return; + + old = World_GetBlock(pos.x, pos.y, pos.z); + if (Blocks.Draw[old] == DRAW_GAS || !Blocks.CanDelete[old]) return; + + Game_ChangeBlock(pos.x, pos.y, pos.z, BLOCK_AIR); + Event_RaiseBlock(&UserEvents.BlockChanged, pos, old, BLOCK_AIR); +} + +void InputHandler_PlaceBlock(void) { + IVec3 pos; + BlockID old, block; + pos = Game_SelectedPos.translatedPos; + if (!Game_SelectedPos.valid || !World_Contains(pos.x, pos.y, pos.z)) return; + + old = World_GetBlock(pos.x, pos.y, pos.z); + block = Inventory_SelectedBlock; + if (AutoRotate_Enabled) block = AutoRotate_RotateBlock(block); + + if (Game_CanPick(old) || !Blocks.CanPlace[block]) return; + /* air-ish blocks can only replace over other air-ish blocks */ + if (Blocks.Draw[block] == DRAW_GAS && Blocks.Draw[old] != DRAW_GAS) return; + + /* undeletable gas blocks can't be replaced with other blocks */ + if (Blocks.Collide[old] == COLLIDE_NONE && !Blocks.CanDelete[old]) return; + + if (!CheckIsFree(block)) return; + + Game_ChangeBlock(pos.x, pos.y, pos.z, block); + Event_RaiseBlock(&UserEvents.BlockChanged, pos, old, block); +} + +void InputHandler_PickBlock(void) { + IVec3 pos; + BlockID cur; + pos = Game_SelectedPos.pos; + if (!World_Contains(pos.x, pos.y, pos.z)) return; + + cur = World_GetBlock(pos.x, pos.y, pos.z); + if (Blocks.Draw[cur] == DRAW_GAS) return; + if (!(Blocks.CanPlace[cur] || Blocks.CanDelete[cur])) return; + Inventory_PickBlock(cur); +} + +void InputHandler_Tick(void) { + cc_bool left, middle, right; + double now, delta; + + if (Gui.InputGrab) return; + now = Game.Time; + delta = now - input_lastClick; + + if (delta < 0.2495) return; /* 4 times per second */ + /* NOTE: 0.2495 is used instead of 0.25 to produce delta time */ + /* values slightly closer to the old code which measured */ + /* elapsed time using DateTime_CurrentUTC_MS() instead */ + input_lastClick = now; + + left = input_buttonsDown[MOUSE_LEFT]; + middle = input_buttonsDown[MOUSE_MIDDLE]; + right = input_buttonsDown[MOUSE_RIGHT]; + +#ifdef CC_BUILD_TOUCH + if (Input_TouchMode) { + left = (Input_HoldMode == INPUT_MODE_DELETE) && AnyBlockTouches(); + right = (Input_HoldMode == INPUT_MODE_PLACE) && AnyBlockTouches(); + middle = false; + } +#endif + + if (Server.SupportsPlayerClick) { + input_pickingId = -1; + if (left) MouseStateUpdate(MOUSE_LEFT, true); + if (right) MouseStateUpdate(MOUSE_RIGHT, true); + if (middle) MouseStateUpdate(MOUSE_MIDDLE, true); + } + + if (left) { + InputHandler_DeleteBlock(); + } else if (right) { + InputHandler_PlaceBlock(); + } else if (middle) { + InputHandler_PickBlock(); + } +} + + +/*########################################################################################################################* +*-----------------------------------------------------Input helpers-------------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool InputHandler_IsShutdown(int key) { + if (key == CCKEY_F4 && Input_IsAltPressed()) return true; + + /* On macOS, Cmd+Q should also end the process */ +#ifdef CC_BUILD_DARWIN + return key == 'Q' && Input_IsWinPressed(); +#else + return false; +#endif +} + +static void InputHandler_Toggle(int key, cc_bool* target, const char* enableMsg, const char* disableMsg) { + *target = !(*target); + if (*target) { + Chat_Add2("%c. &ePress &a%c &eto disable.", enableMsg, Input_DisplayNames[key]); + } else { + Chat_Add2("%c. &ePress &a%c &eto re-enable.", disableMsg, Input_DisplayNames[key]); + } +} + +cc_bool InputHandler_SetFOV(int fov) { + struct HacksComp* h = &Entities.CurPlayer->Hacks; + if (!h->Enabled || !h->CanUseThirdPerson) return false; + + Camera.ZoomFov = fov; + Camera_SetFov(fov); + return true; +} + +cc_bool Input_HandleMouseWheel(float delta) { + struct HacksComp* h; + cc_bool hotbar; + + hotbar = Input_IsAltPressed() || Input_IsCtrlPressed() || Input_IsShiftPressed(); + if (!hotbar && Camera.Active->Zoom(delta)) return true; + if (!InputBind_IsPressed(BIND_ZOOM_SCROLL)) return false; + + h = &Entities.CurPlayer->Hacks; + if (!h->Enabled || !h->CanUseThirdPerson) return false; + + if (input_fovIndex == -1.0f) input_fovIndex = (float)Camera.ZoomFov; + input_fovIndex -= delta * 5.0f; + + Math_Clamp(input_fovIndex, 1.0f, Camera.DefaultFov); + return InputHandler_SetFOV((int)input_fovIndex); +} + +static void InputHandler_CheckZoomFov(void* obj) { + struct HacksComp* h = &Entities.CurPlayer->Hacks; + if (!h->Enabled || !h->CanUseThirdPerson) Camera_SetFov(Camera.DefaultFov); +} + + +static cc_bool BindTriggered_DeleteBlock(int key) { + MouseStatePress(MOUSE_LEFT); + InputHandler_DeleteBlock(); + return true; +} + +static cc_bool BindTriggered_PlaceBlock(int key) { + MouseStatePress(MOUSE_RIGHT); + InputHandler_PlaceBlock(); + return true; +} + +static cc_bool BindTriggered_PickBlock(int key) { + MouseStatePress(MOUSE_MIDDLE); + InputHandler_PickBlock(); + return true; +} + +static void BindReleased_DeleteBlock(int key) { + MouseStateRelease(MOUSE_LEFT); +} + +static void BindReleased_PlaceBlock(int key) { + MouseStateRelease(MOUSE_RIGHT); +} + +static void BindReleased_PickBlock(int key) { + MouseStateRelease(MOUSE_MIDDLE); +} + + +static cc_bool BindTriggered_HideFPS(int key) { + Gui.ShowFPS = !Gui.ShowFPS; + return true; +} + +static cc_bool BindTriggered_Fullscreen(int key) { + Game_ToggleFullscreen(); + return true; +} + +static cc_bool BindTriggered_Fog(int key) { + Game_CycleViewDistance(); + return true; +} + + +static cc_bool BindTriggered_HideGUI(int key) { + Game_HideGui = !Game_HideGui; + return true; +} + +static cc_bool BindTriggered_SmoothCamera(int key) { + InputHandler_Toggle(key, &Camera.Smooth, + " &eSmooth camera is &aenabled", + " &eSmooth camera is &cdisabled"); + return true; +} + +static cc_bool BindTriggered_AxisLines(int key) { + InputHandler_Toggle(key, &AxisLinesRenderer_Enabled, + " &eAxis lines (&4X&e, &2Y&e, &1Z&e) now show", + " &eAxis lines no longer show"); + return true; +} + +static cc_bool BindTriggered_AutoRotate(int key) { + InputHandler_Toggle(key, &AutoRotate_Enabled, + " &eAuto rotate is &aenabled", + " &eAuto rotate is &cdisabled"); + return true; +} + +static cc_bool BindTriggered_ThirdPerson(int key) { + Camera_CycleActive(); + return true; +} + +static cc_bool BindTriggered_DropBlock(int key) { + if (Inventory_CheckChangeSelected() && Inventory_SelectedBlock != BLOCK_AIR) { + /* Don't assign SelectedIndex directly, because we don't want held block + switching positions if they already have air in their inventory hotbar. */ + Inventory_Set(Inventory.SelectedIndex, BLOCK_AIR); + Event_RaiseVoid(&UserEvents.HeldBlockChanged); + } + return true; +} + +static cc_bool BindTriggered_IDOverlay(int key) { + TexIdsOverlay_Show(); + return true; +} + +static cc_bool BindTriggered_BreakLiquids(int key) { + InputHandler_Toggle(key, &Game_BreakableLiquids, + " &eBreakable liquids is &aenabled", + " &eBreakable liquids is &cdisabled"); + return true; +} + +static void HandleHotkeyDown(int key) { + struct HotkeyData* hkey; + cc_string text; + int i = Hotkeys_FindPartial(key); + + if (i == -1) return; + hkey = &HotkeysList[i]; + text = StringsBuffer_UNSAFE_Get(&HotkeysText, hkey->textIndex); + + if (!(hkey->flags & HOTKEY_FLAG_STAYS_OPEN)) { + Chat_Send(&text, false); + } else if (!Gui.InputGrab) { + ChatScreen_OpenInput(&text); + } +} + +static void HookInputBinds(void) { + Bind_OnTriggered[BIND_HIDE_FPS] = BindTriggered_HideFPS; + Bind_OnTriggered[BIND_FULLSCREEN] = BindTriggered_Fullscreen; + Bind_OnTriggered[BIND_FOG] = BindTriggered_Fog; + + Bind_OnTriggered[BIND_DELETE_BLOCK] = BindTriggered_DeleteBlock; + Bind_OnTriggered[BIND_PLACE_BLOCK] = BindTriggered_PlaceBlock; + Bind_OnTriggered[BIND_PICK_BLOCK] = BindTriggered_PickBlock; + + Bind_OnReleased[BIND_DELETE_BLOCK] = BindReleased_DeleteBlock; + Bind_OnReleased[BIND_PLACE_BLOCK] = BindReleased_PlaceBlock; + Bind_OnReleased[BIND_PICK_BLOCK] = BindReleased_PickBlock; + + if (Game_ClassicMode) return; + Bind_OnTriggered[BIND_HIDE_GUI] = BindTriggered_HideGUI; + Bind_OnTriggered[BIND_SMOOTH_CAMERA] = BindTriggered_SmoothCamera; + Bind_OnTriggered[BIND_AXIS_LINES] = BindTriggered_AxisLines; + Bind_OnTriggered[BIND_AUTOROTATE] = BindTriggered_AutoRotate; + Bind_OnTriggered[BIND_THIRD_PERSON] = BindTriggered_ThirdPerson; + Bind_OnTriggered[BIND_DROP_BLOCK] = BindTriggered_DropBlock; + Bind_OnTriggered[BIND_IDOVERLAY] = BindTriggered_IDOverlay; + Bind_OnTriggered[BIND_BREAK_LIQUIDS] = BindTriggered_BreakLiquids; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Base handlers-------------------------------------------------------* +*#########################################################################################################################*/ +static void OnPointerDown(void* obj, int idx) { + struct Screen* s; + int i, x, y, mask; +#ifdef CC_BUILD_TOUCH + if (Input_TouchMode && !(touches[idx].type & TOUCH_TYPE_GUI)) return; +#endif + x = Pointers[idx].x; y = Pointers[idx].y; + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + s->dirty = true; + mask = s->VTABLE->HandlesPointerDown(s, 1 << idx, x, y); + +#ifdef CC_BUILD_TOUCH + if (mask) { + /* Using &= mask instead of = mask is to handle one specific case */ + /* - when clicking 'Quit game' in android version, it will call */ + /* Game_Free, which will in turn call InputComponent.Free. */ + /* That resets the type of all touches to 0 - however, since it is */ + /* called DURING HandlesPointerDown, using = mask here would undo */ + /* the resetting of type to 0 for one of the touches states, */ + /* causing problems later with Input_AddTouch as it will assume that */ + /* the aforementioned touches state is wrongly still in use */ + touches[idx].type &= mask; return; + } +#else + if (mask) return; +#endif + } +} + +static void OnPointerUp(void* obj, int idx) { + struct Screen* s; + int i, x, y; +#ifdef CC_BUILD_TOUCH + CheckBlockTap(idx); + if (Input_TouchMode && !(touches[idx].type & TOUCH_TYPE_GUI)) return; +#endif + x = Pointers[idx].x; y = Pointers[idx].y; + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + s->dirty = true; + s->VTABLE->OnPointerUp(s, 1 << idx, x, y); + } +} + +static void OnInputDown(void* obj, int key, cc_bool was) { + struct Screen* s; + cc_bool triggered; + int i; + if (Window_Main.SoftKeyboardFocus) return; + +#ifndef CC_BUILD_WEB + if (Input_IsEscapeButton(key) && (s = Gui_GetClosable())) { + /* Don't want holding down escape to go in and out of pause menu */ + if (!was) Gui_Remove(s); + return; + } +#endif + + if (InputHandler_IsShutdown(key)) { + /* TODO: Do we need a separate exit function in Game class? */ + Window_RequestClose(); return; + } else if (InputBind_Claims(BIND_SCREENSHOT, key) && !was) { + Game_ScreenshotRequested = true; return; + } + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + s->dirty = true; + if (s->VTABLE->HandlesInputDown(s, key)) return; + } + if (Gui.InputGrab) return; + + if (Input_IsPauseButton(key)) { +#ifdef CC_BUILD_WEB + /* Can't do this in KeyUp, because pressing escape without having */ + /* explicitly disabled mouse lock means a KeyUp event isn't sent. */ + /* But switching to pause screen disables mouse lock, causing a KeyUp */ + /* event to be sent, triggering the active->closable case which immediately */ + /* closes the pause screen. Hence why the next KeyUp must be supressed. */ + suppressEscape = true; +#endif + Gui_ShowPauseMenu(); return; + } + + /* These should not be triggered multiple times when holding down */ + if (was) return; + triggered = false; + + for (i = 0; i < BIND_COUNT; i++) + { + if (!Bind_OnTriggered[i]) continue; + if (!InputBind_Claims(i, key)) continue; + + triggered |= Bind_OnTriggered[i](key); + } + + if (triggered) { + } else if (key == CCKEY_F5 && Game_ClassicMode) { + int weather = Env.Weather == WEATHER_SUNNY ? WEATHER_RAINY : WEATHER_SUNNY; + Env_SetWeather(weather); + } else { HandleHotkeyDown(key); } +} + +static void OnInputUp(void* obj, int key) { + struct Screen* s; + int i; + + if (InputBind_Claims(BIND_ZOOM_SCROLL, key)) Camera_SetFov(Camera.DefaultFov); +#ifdef CC_BUILD_WEB + /* When closing menus (which reacquires mouse focus) in key down, */ + /* this still leaves the cursor visible. But if this is instead */ + /* done in key up, the cursor disappears as expected. */ + if (key == CCKEY_ESCAPE && (s = Gui_GetClosable())) { + if (suppressEscape) { suppressEscape = false; return; } + Gui_Remove(s); return; + } +#endif + + for (i = 0; i < Gui.ScreensCount; i++) { + s = Gui_Screens[i]; + s->dirty = true; + s->VTABLE->OnInputUp(s, key); + } + + for (i = 0; i < BIND_COUNT; i++) + { + if (!Bind_OnReleased[i]) continue; + if (!InputBind_Claims(i, key)) continue; + + Bind_OnReleased[i](key); + } +} + +static void OnFocusChanged(void* obj) { if (!Window_Main.Focused) Input_Clear(); } + +static void PlayerInputNormal(struct LocalPlayer* p, float* xMoving, float* zMoving) { + if (InputBind_IsPressed(BIND_FORWARD)) *zMoving -= 1; + if (InputBind_IsPressed(BIND_BACK)) *zMoving += 1; + if (InputBind_IsPressed(BIND_LEFT)) *xMoving -= 1; + if (InputBind_IsPressed(BIND_RIGHT)) *xMoving += 1; +} +static struct LocalPlayerInput normalInput = { PlayerInputNormal }; + +static void OnInit(void) { + LocalPlayerInput_Add(&normalInput); + LocalPlayerInput_Add(&gamepadInput); + HookInputBinds(); + + Event_Register_(&PointerEvents.Down, NULL, OnPointerDown); + Event_Register_(&PointerEvents.Up, NULL, OnPointerUp); + Event_Register_(&InputEvents.Down, NULL, OnInputDown); + Event_Register_(&InputEvents.Up, NULL, OnInputUp); + + Event_Register_(&WindowEvents.FocusChanged, NULL, OnFocusChanged); + Event_Register_(&UserEvents.HackPermsChanged, NULL, InputHandler_CheckZoomFov); + KeyBind_Init(); + StoredHotkeys_LoadAll(); + /* Fix issue with Android where if you double click in server list to join, a touch */ + /* pointer is stuck down when the game loads (so you instantly start deleting blocks) */ + ClearTouches(); +} + +static void OnFree(void) { + ClearTouches(); + HotkeysText.count = 0; +} + +struct IGameComponent Input_Component = { + OnInit, /* Init */ + OnFree, /* Free */ +}; diff --git a/src/Input.h b/src/Input.h new file mode 100644 index 0000000..e4e491e --- /dev/null +++ b/src/Input.h @@ -0,0 +1,272 @@ +#ifndef CC_INPUT_H +#define CC_INPUT_H +#include "Core.h" +/* +Manages input state, raising input related events, and base input handling +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +struct StringsBuffer; +extern struct IGameComponent Input_Component; + +enum InputButtons { + INPUT_NONE, /* Unrecognised key */ + + CCKEY_F1, CCKEY_F2, CCKEY_F3, CCKEY_F4, CCKEY_F5, CCKEY_F6, CCKEY_F7, CCKEY_F8, CCKEY_F9, CCKEY_F10, + CCKEY_F11, CCKEY_F12, CCKEY_F13, CCKEY_F14, CCKEY_F15, CCKEY_F16, CCKEY_F17, CCKEY_F18, CCKEY_F19, CCKEY_F20, + CCKEY_F21, CCKEY_F22, CCKEY_F23, CCKEY_F24, + + CCKEY_TILDE, CCKEY_MINUS, CCKEY_EQUALS, CCKEY_LBRACKET, CCKEY_RBRACKET, CCKEY_SLASH, + CCKEY_SEMICOLON, CCKEY_QUOTE, CCKEY_COMMA, CCKEY_PERIOD, CCKEY_BACKSLASH, + + CCKEY_LSHIFT, CCKEY_RSHIFT, CCKEY_LCTRL, CCKEY_RCTRL, + CCKEY_LALT, CCKEY_RALT, CCKEY_LWIN, CCKEY_RWIN, + + CCKEY_UP, CCKEY_DOWN, CCKEY_LEFT, CCKEY_RIGHT, + + CCKEY_0, CCKEY_1, CCKEY_2, CCKEY_3, CCKEY_4, + CCKEY_5, CCKEY_6, CCKEY_7, CCKEY_8, CCKEY_9, /* same as '0'-'9' */ + + CCKEY_INSERT, CCKEY_DELETE, CCKEY_HOME, CCKEY_END, CCKEY_PAGEUP, CCKEY_PAGEDOWN, + CCKEY_MENU, + + CCKEY_A, CCKEY_B, CCKEY_C, CCKEY_D, CCKEY_E, CCKEY_F, CCKEY_G, CCKEY_H, CCKEY_I, CCKEY_J, + CCKEY_K, CCKEY_L, CCKEY_M, CCKEY_N, CCKEY_O, CCKEY_P, CCKEY_Q, CCKEY_R, CCKEY_S, CCKEY_T, + CCKEY_U, CCKEY_V, CCKEY_W, CCKEY_X, CCKEY_Y, CCKEY_Z, /* same as 'A'-'Z' */ + + CCKEY_ENTER, CCKEY_ESCAPE, CCKEY_SPACE, CCKEY_BACKSPACE, CCKEY_TAB, CCKEY_CAPSLOCK, + CCKEY_SCROLLLOCK, CCKEY_PRINTSCREEN, CCKEY_PAUSE, CCKEY_NUMLOCK, + + CCKEY_KP0, CCKEY_KP1, CCKEY_KP2, CCKEY_KP3, CCKEY_KP4, + CCKEY_KP5, CCKEY_KP6, CCKEY_KP7, CCKEY_KP8, CCKEY_KP9, + CCKEY_KP_DIVIDE, CCKEY_KP_MULTIPLY, CCKEY_KP_MINUS, + CCKEY_KP_PLUS, CCKEY_KP_DECIMAL, CCKEY_KP_ENTER, + + /* NOTE: RMOUSE must be before MMOUSE for PlayerClick compatibility */ + CCMOUSE_X1, CCMOUSE_X2, CCMOUSE_L, CCMOUSE_R, CCMOUSE_M, + CCWHEEL_UP, CCWHEEL_DOWN, CCWHEEL_LEFT, CCWHEEL_RIGHT, + CCMOUSE_X3, CCMOUSE_X4, CCMOUSE_X5, CCMOUSE_X6, + + CCKEY_VOLUME_MUTE, CCKEY_VOLUME_UP, CCKEY_VOLUME_DOWN, CCKEY_SLEEP, + CCKEY_MEDIA_NEXT, CCKEY_MEDIA_PREV, CCKEY_MEDIA_PLAY, CCKEY_MEDIA_STOP, + CCKEY_BROWSER_PREV, CCKEY_BROWSER_NEXT, CCKEY_BROWSER_REFRESH, CCKEY_BROWSER_STOP, CCKEY_BROWSER_SEARCH, CCKEY_BROWSER_FAVORITES, CCKEY_BROWSER_HOME, + CCKEY_LAUNCH_MAIL, CCKEY_LAUNCH_MEDIA, CCKEY_LAUNCH_APP1, CCKEY_LAUNCH_CALC, + + CCPAD_A, CCPAD_B, CCPAD_X, CCPAD_Y, CCPAD_L, CCPAD_R, + CCPAD_Z, CCPAD_C, CCPAD_D, + CCPAD_LEFT, CCPAD_RIGHT, CCPAD_UP, CCPAD_DOWN, + CCPAD_START, CCPAD_SELECT, CCPAD_ZL, CCPAD_ZR, + CCPAD_LSTICK, CCPAD_RSTICK, + CCPAD_CLEFT, CCPAD_CRIGHT, CCPAD_CUP, CCPAD_CDOWN, + + INPUT_COUNT, + + INPUT_CLIPBOARD_COPY = 1001, + INPUT_CLIPBOARD_PASTE = 1002 +}; +#define Input_IsPadButton(btn) ((btn) >= CCPAD_A && (btn) < INPUT_COUNT) + +extern const char* const Input_DisplayNames[INPUT_COUNT]; + +extern struct _InputState { + /* Pressed state of each input button. Use Input_Set to change */ + cc_bool Pressed[INPUT_COUNT]; + /* Whether raw mouse/touch input is currently being listened for */ + cc_bool RawMode; + /* Sources available for input (Mouse/Keyboard, Gamepad) */ + cc_uint8 Sources; +} Input; + +#define INPUT_SOURCE_NORMAL (1 << 0) +#define INPUT_SOURCE_GAMEPAD (1 << 1) + +/* Sets Input_Pressed[key] to true and raises InputEvents.Down */ +void Input_SetPressed(int key); +/* Sets Input_Pressed[key] to false and raises InputEvents.Up */ +void Input_SetReleased(int key); +/* Calls either Input_SetPressed or Input_SetReleased */ +void Input_Set(int key, int pressed); +void Input_SetNonRepeatable(int key, int pressed); +/* Resets all input buttons to released state. (Input_SetReleased) */ +void Input_Clear(void); + + +#define Input_IsWinPressed() (Input.Pressed[CCKEY_LWIN] || Input.Pressed[CCKEY_RWIN]) +#define Input_IsAltPressed() (Input.Pressed[CCKEY_LALT] || Input.Pressed[CCKEY_RALT]) +#define Input_IsCtrlPressed() (Input.Pressed[CCKEY_LCTRL] || Input.Pressed[CCKEY_RCTRL]) +#define Input_IsShiftPressed() (Input.Pressed[CCKEY_LSHIFT] || Input.Pressed[CCKEY_RSHIFT]) + +#define Input_IsUpButton(btn) ((btn) == CCKEY_UP || (btn) == CCPAD_UP) +#define Input_IsDownButton(btn) ((btn) == CCKEY_DOWN || (btn) == CCPAD_DOWN) +#define Input_IsLeftButton(btn) ((btn) == CCKEY_LEFT || (btn) == CCPAD_LEFT) +#define Input_IsRightButton(btn) ((btn) == CCKEY_RIGHT || (btn) == CCPAD_RIGHT) + +#define Input_IsEnterButton(btn) ((btn) == CCKEY_ENTER || (btn) == CCPAD_START || (btn) == CCKEY_KP_ENTER) +#define Input_IsPauseButton(btn) ((btn) == CCKEY_ESCAPE || (btn) == CCPAD_START || (btn) == CCKEY_PAUSE) +#define Input_IsEscapeButton(btn) ((btn) == CCKEY_ESCAPE || (btn) == CCPAD_SELECT) + +#if defined CC_BUILD_HAIKU + /* Haiku uses ALT instead of CTRL for clipboard and stuff */ + #define Input_IsActionPressed() Input_IsAltPressed() +#elif defined CC_BUILD_DARWIN + /* macOS uses CMD instead of CTRL for clipboard and stuff */ + #define Input_IsActionPressed() Input_IsWinPressed() +#else + #define Input_IsActionPressed() Input_IsCtrlPressed() +#endif +int Input_CalcDelta(int btn, int horDelta, int verDelta); + + +#ifdef CC_BUILD_TOUCH +#define INPUT_MAX_POINTERS 32 +enum INPUT_MODE { INPUT_MODE_PLACE, INPUT_MODE_DELETE, INPUT_MODE_NONE, INPUT_MODE_COUNT }; + +extern int Pointers_Count; +extern int Input_TapMode, Input_HoldMode; +/* Whether touch input is being used. */ +extern cc_bool Input_TouchMode; +void Input_SetTouchMode(cc_bool enabled); + +void Input_AddTouch(long id, int x, int y); +void Input_UpdateTouch(long id, int x, int y); +void Input_RemoveTouch(long id, int x, int y); +#else +#define INPUT_MAX_POINTERS 1 +#define Pointers_Count 1 +#define Input_TouchMode false +#endif + +/* Touch fingers are initially are 'all' type, meaning they could */ +/* trigger menu clicks, camera movement, or place/delete blocks */ +/* But for example, after clicking on a menu button, you wouldn't */ +/* want moving that finger anymore to move the camera */ +#define TOUCH_TYPE_GUI 1 +#define TOUCH_TYPE_CAMERA 2 +#define TOUCH_TYPE_BLOCKS 4 +#define TOUCH_TYPE_ALL (TOUCH_TYPE_GUI | TOUCH_TYPE_CAMERA | TOUCH_TYPE_BLOCKS) + +/* Data for mouse and touch */ +extern struct Pointer { int x, y; } Pointers[INPUT_MAX_POINTERS]; +/* Raises appropriate events for a mouse vertical scroll */ +void Mouse_ScrollVWheel(float delta); +/* Raises appropriate events for a mouse horizontal scroll */ +void Mouse_ScrollHWheel(float delta); +/* Sets X and Y position of the given pointer, then raises appropriate events */ +void Pointer_SetPosition(int idx, int x, int y); + + +/* Enumeration of all input bindings. */ +enum InputBind_ { + BIND_FORWARD, BIND_BACK, BIND_LEFT, BIND_RIGHT, + BIND_JUMP, BIND_RESPAWN, BIND_SET_SPAWN, BIND_CHAT, + BIND_INVENTORY, BIND_FOG, BIND_SEND_CHAT, BIND_TABLIST, + BIND_SPEED, BIND_NOCLIP, BIND_FLY, BIND_FLY_UP, BIND_FLY_DOWN, + BIND_EXT_INPUT, BIND_HIDE_FPS, BIND_SCREENSHOT, BIND_FULLSCREEN, + BIND_THIRD_PERSON, BIND_HIDE_GUI, BIND_AXIS_LINES, BIND_ZOOM_SCROLL, + BIND_HALF_SPEED, BIND_DELETE_BLOCK, BIND_PICK_BLOCK, BIND_PLACE_BLOCK, + BIND_AUTOROTATE, BIND_HOTBAR_SWITCH, BIND_SMOOTH_CAMERA, + BIND_DROP_BLOCK, BIND_IDOVERLAY, BIND_BREAK_LIQUIDS, + BIND_LOOK_UP, BIND_LOOK_DOWN, BIND_LOOK_RIGHT, BIND_LOOK_LEFT, + BIND_HOTBAR_1, BIND_HOTBAR_2, BIND_HOTBAR_3, + BIND_HOTBAR_4, BIND_HOTBAR_5, BIND_HOTBAR_6, + BIND_HOTBAR_7, BIND_HOTBAR_8, BIND_HOTBAR_9, + BIND_HOTBAR_LEFT, BIND_HOTBAR_RIGHT, + BIND_COUNT +}; +typedef int InputBind; +typedef struct BindMapping_ { cc_uint8 button1, button2; } BindMapping; +typedef cc_bool (*BindTriggered)(int key); +typedef void (*BindReleased)(int key); +#define BindMapping_Set(mapping, btn1, btn2) (mapping)->button1 = btn1; (mapping)->button2 = btn2; + +/* The keyboard/mouse buttons that are bound to each input binding */ +extern BindMapping KeyBind_Mappings[BIND_COUNT]; +/* The gamepad buttons that are bound to each input binding */ +extern BindMapping PadBind_Mappings[BIND_COUNT]; +/* Default keyboard/mouse button that each input binding is bound to */ +extern const BindMapping KeyBind_Defaults[BIND_COUNT]; +/* Default gamepad button that each input binding is bound to */ +extern const BindMapping PadBind_Defaults[BIND_COUNT]; +/* Callback behaviour for when the given input binding is triggered */ +extern BindTriggered Bind_OnTriggered[BIND_COUNT]; +/* Callback behaviour for when the given input binding is released */ +extern BindReleased Bind_OnReleased[BIND_COUNT]; + +/* InputBind_IsPressed is what should be used, but export KeyBind_IsPressed for backwards compatibility */ +#define InputBind_IsPressed KeyBind_IsPressed +/* Whether the given binding should be triggered in response to given input button being pressed */ +CC_API cc_bool InputBind_Claims(InputBind binding, int btn); +/* Gets whether the given input binding is currently being triggered */ +CC_API cc_bool InputBind_IsPressed(InputBind binding); + +/* Sets the key/mouse button that the given input binding is bound to */ +void KeyBind_Set(InputBind binding, int btn); +/* Sets the gamepad button that the given input binding is bound to */ +void PadBind_Set(InputBind binding, int btn); +/* Resets the key/mouse button that the given input binding is bound to */ +void KeyBind_Reset(InputBind binding); +/* Resets the gamepad button that the given input binding is bound to*/ +void PadBind_Reset(InputBind binding); + + +/* Gamepad axes. Default behaviour is: */ +/* - left axis: player movement */ +/* - right axis: camera movement */ +enum PAD_AXIS { PAD_AXIS_LEFT, PAD_AXIS_RIGHT }; +enum AXIS_SENSITIVITY { AXIS_SENSI_LOWER, AXIS_SENSI_LOW, AXIS_SENSI_NORMAL, AXIS_SENSI_HIGH, AXIS_SENSI_HIGHER }; +enum AXIS_BEHAVIOUR { AXIS_BEHAVIOUR_MOVEMENT, AXIS_BEHAVIOUR_CAMERA }; +extern int Gamepad_AxisBehaviour[2]; +extern int Gamepad_AxisSensitivity[2]; + +/* Sets value of the given gamepad button */ +void Gamepad_SetButton(int port, int btn, int pressed); +/* Sets value of the given axis */ +void Gamepad_SetAxis(int port, int axis, float x, float y, float delta); +void Gamepad_Tick(float delta); +#define INPUT_MAX_GAMEPADS 4 + + +/* whether to leave text input open for user to enter further input */ +#define HOTKEY_FLAG_STAYS_OPEN 0x01 +/* Whether the hotkey was auto defined (e.g. by server) */ +#define HOTKEY_FLAG_AUTO_DEFINED 0x02 + +extern const cc_uint8 Hotkeys_LWJGL[256]; +struct HotkeyData { + int textIndex; /* contents to copy directly into the input bar */ + cc_uint8 trigger; /* Member of Key enumeration */ + cc_uint8 mods; /* HotkeyModifiers bitflags */ + cc_uint8 flags; /* HOTKEY_FLAG flags */ +}; + +#define HOTKEYS_MAX_COUNT 256 +extern struct HotkeyData HotkeysList[HOTKEYS_MAX_COUNT]; +extern struct StringsBuffer HotkeysText; +enum HotkeyModifiers { + HOTKEY_MOD_CTRL = 1, HOTKEY_MOD_SHIFT = 2, HOTKEY_MOD_ALT = 4 +}; + +/* Adds or updates a new hotkey. */ +void Hotkeys_Add(int trigger, cc_uint8 modifiers, const cc_string* text, cc_uint8 flags); +/* Removes the given hotkey. */ +cc_bool Hotkeys_Remove(int trigger, cc_uint8 modifiers); +/* Returns the first hotkey which is bound to the given key and has its modifiers pressed. */ +/* NOTE: The hotkeys list is sorted, so hotkeys with most modifiers are checked first. */ +int Hotkeys_FindPartial(int key); + +/* Loads the given hotkey from options. (if it exists) */ +void StoredHotkeys_Load(int trigger, cc_uint8 modifiers); +/* Removes the given hotkey from options. */ +void StoredHotkeys_Remove(int trigger, cc_uint8 modifiers); +/* Adds the given hotkey from options. */ +void StoredHotkeys_Add(int trigger, cc_uint8 modifiers, cc_bool moreInput, const cc_string* text); + + +cc_bool InputHandler_SetFOV(int fov); +cc_bool Input_HandleMouseWheel(float delta); +void InputHandler_Tick(void); +void InputHandler_OnScreensChanged(void); +void InputHandler_DeleteBlock(void); +void InputHandler_PlaceBlock(void); +void InputHandler_PickBlock(void); +#endif diff --git a/src/Inventory.c b/src/Inventory.c new file mode 100644 index 0000000..5c81ee9 --- /dev/null +++ b/src/Inventory.c @@ -0,0 +1,154 @@ +#include "Inventory.h" +#include "Funcs.h" +#include "Game.h" +#include "Block.h" +#include "Event.h" +#include "Chat.h" + +struct _InventoryData Inventory; + +cc_bool Inventory_CheckChangeSelected(void) { + if (!Inventory.CanChangeSelected) { + Chat_AddRaw("&cThe server has forbidden you from changing your held block."); + return false; + } + return true; +} + +void Inventory_SetSelectedIndex(int index) { + if (!Inventory_CheckChangeSelected()) return; + Inventory.SelectedIndex = index; + Event_RaiseVoid(&UserEvents.HeldBlockChanged); +} + +void Inventory_SetHotbarIndex(int index) { + if (!Inventory_CheckChangeSelected() || Game_ClassicMode) return; + Inventory.Offset = index * INVENTORY_BLOCKS_PER_HOTBAR; + Event_RaiseVoid(&UserEvents.HeldBlockChanged); +} + +void Inventory_SwitchHotbar(void) { + int index = Inventory.Offset == 0 ? 1 : 0; + Inventory_SetHotbarIndex(index); +} + +void Inventory_SetSelectedBlock(BlockID block) { + int i; + if (!Inventory_CheckChangeSelected()) return; + + /* Swap with currently selected block if given block is already in the hotbar */ + for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) { + if (Inventory_Get(i) != block) continue; + Inventory_Set(i, Inventory_SelectedBlock); + break; + } + + Inventory_Set(Inventory.SelectedIndex, block); + Event_RaiseVoid(&UserEvents.HeldBlockChanged); +} + +void Inventory_PickBlock(BlockID block) { + int i; + if (!Inventory_CheckChangeSelected() || Inventory_SelectedBlock == block) return; + + /* Vanilla classic client doesn't let you select these blocks */ + if (Game_PureClassic) { + if (block == BLOCK_GRASS) block = BLOCK_DIRT; + if (block == BLOCK_DOUBLE_SLAB) block = BLOCK_SLAB; + } + + /* Try to replace same block */ + for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) { + if (Inventory_Get(i) != block) continue; + Inventory_SetSelectedIndex(i); return; + } + + if (AutoRotate_Enabled) { + /* Try to replace existing autorotate variant */ + for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) { + if (AutoRotate_BlocksShareGroup(Inventory_Get(i), block)) { + Inventory_SetSelectedIndex(i); + Inventory_SetSelectedBlock(block); + return; + } + } + } + + /* Is the currently selected block an empty slot? */ + if (Inventory_SelectedBlock == BLOCK_AIR) { + Inventory_SetSelectedBlock(block); return; + } + + /* Try to replace empty slots */ + for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) { + if (Inventory_Get(i) != BLOCK_AIR) continue; + Inventory_Set(i, block); + Inventory_SetSelectedIndex(i); return; + } + + /* Finally, replace the currently selected block */ + Inventory_SetSelectedBlock(block); +} + +/* Returns default block that should go in the given inventory slot */ +static BlockID DefaultMapping(int slot) { + if (Game_ClassicMode) { + if (slot < Game_Version.InventorySize) return Game_Version.Inventory[slot]; + } else if (slot < Game_Version.MaxCoreBlock) { + return (BlockID)(slot + 1); + } + return BLOCK_AIR; +} + +void Inventory_ResetMapping(void) { + int slot; + for (slot = 0; slot < Array_Elems(Inventory.Map); slot++) { + Inventory.Map[slot] = DefaultMapping(slot); + } +} + +void Inventory_AddDefault(BlockID block) { + int slot; + if (block > BLOCK_MAX_CPE) { + Inventory.Map[block - 1] = block; return; + } + + for (slot = 0; slot < BLOCK_MAX_CPE; slot++) { + if (DefaultMapping(slot) != block) continue; + Inventory.Map[slot] = block; + return; + } +} + +void Inventory_Remove(BlockID block) { + int slot; + for (slot = 0; slot < Array_Elems(Inventory.Map); slot++) { + if (Inventory.Map[slot] == block) Inventory.Map[slot] = BLOCK_AIR; + } +} + + +/*########################################################################################################################* +*--------------------------------------------------Inventory component----------------------------------------------------* +*#########################################################################################################################*/ +static void OnReset(void) { + Inventory_ResetMapping(); + Inventory.CanChangeSelected = true; +} + +static void OnInit(void) { + int i; + BlockID* inv = Inventory.Table; + OnReset(); + Inventory.BlocksPerRow = Game_Version.BlocksPerRow; + + for (i = 0; i < INVENTORY_BLOCKS_PER_HOTBAR; i++) { + inv[i] = Game_Version.Hotbar[i]; + } +} + +struct IGameComponent Inventory_Component = { + OnInit, /* Init */ + NULL, /* Free */ + OnReset, /* Reset */ +}; diff --git a/src/Inventory.h b/src/Inventory.h new file mode 100644 index 0000000..6025e21 --- /dev/null +++ b/src/Inventory.h @@ -0,0 +1,63 @@ +#ifndef CC_INVENTORY_H +#define CC_INVENTORY_H +#include "Core.h" +#include "BlockID.h" + +/* Manages inventory hotbar, and ordering of blocks in the inventory menu. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent Inventory_Component; + +/* Number of blocks in each hotbar */ +#define INVENTORY_BLOCKS_PER_HOTBAR 9 +/* Number of hotbars that can be selected between */ +#define INVENTORY_HOTBARS 9 +#define HOTBAR_MAX_INDEX (INVENTORY_BLOCKS_PER_HOTBAR - 1) + +CC_VAR extern struct _InventoryData { + /* Stores the currently bound blocks for all hotbars. */ + BlockID Table[INVENTORY_HOTBARS * INVENTORY_BLOCKS_PER_HOTBAR]; + /* Mapping of indices in inventory menu to block IDs. */ + BlockID Map[BLOCK_COUNT]; + /* Currently selected index within a hotbar. */ + int SelectedIndex; + /* Currently selected hotbar. */ + int Offset; + /* Whether the user is allowed to change selected/held block. */ + cc_bool CanChangeSelected; + /* Number of blocks in each row in inventory menu. */ + cc_uint8 BlocksPerRow; +} Inventory; + +/* Gets the block at the nth index in the current hotbar. */ +#define Inventory_Get(idx) (Inventory.Table[Inventory.Offset + (idx)]) +/* Sets the block at the nth index in the current hotbar. */ +#define Inventory_Set(idx, block) Inventory.Table[Inventory.Offset + (idx)] = block +/* Gets the currently selected block. */ +#define Inventory_SelectedBlock Inventory_Get(Inventory.SelectedIndex) + +/* Checks if the user can change their selected/held block. */ +/* NOTE: Shows a message in chat if they are unable to. */ +cc_bool Inventory_CheckChangeSelected(void); +/* Attempts to set the currently selected index in a hotbar. */ +void Inventory_SetSelectedIndex(int index); +/* Attempts to set the currently active hotbar. */ +void Inventory_SetHotbarIndex(int index); +void Inventory_SwitchHotbar(void); +/* Attempts to set the block for the selected index in the current hotbar. */ +/* NOTE: If another slot is already this block, the selected index is instead changed. */ +void Inventory_SetSelectedBlock(BlockID block); +/* Attempts to set the selected block in a user-friendly manner. */ +/* e.g. this method tries to replace empty slots before other blocks */ +void Inventory_PickBlock(BlockID block); +/* Sets all slots to contain their default associated block. */ +/* NOTE: The order of default blocks may not be in order of ID. */ +void Inventory_ResetMapping(void); + +/* Inserts the given block at its default slot in the inventory. */ +/* NOTE: Replaces (doesn't move) the block that was at that slot before. */ +void Inventory_AddDefault(BlockID block); +/* Removes any slots with the given block from the inventory. */ +void Inventory_Remove(BlockID block); +#endif diff --git a/src/IsometricDrawer.c b/src/IsometricDrawer.c new file mode 100644 index 0000000..6e500c5 --- /dev/null +++ b/src/IsometricDrawer.c @@ -0,0 +1,170 @@ +#include "IsometricDrawer.h" +#include "Drawer.h" +#include "Graphics.h" +#include "PackedCol.h" +#include "ExtMath.h" +#include "Block.h" +#include "TexturePack.h" +#include "Block.h" +#include "Game.h" + +static struct VertexTextured* iso_vertices; +static struct VertexTextured* iso_vertices_base; +static int* iso_state; + +static cc_bool iso_cacheInited; +static PackedCol iso_colorXSide, iso_colorZSide, iso_colorYBottom; +static float iso_posX, iso_posY; + +#define iso_cosX (0.86602540378443864f) /* cos(30 * MATH_DEG2RAD) */ +#define iso_sinX (0.50000000000000000f) /* sin(30 * MATH_DEG2RAD) */ +#define iso_cosY (0.70710678118654752f) /* cos(-45 * MATH_DEG2RAD) */ +#define iso_sinY (-0.70710678118654752f) /* sin(-45 * MATH_DEG2RAD) */ + +static void IsometricDrawer_InitCache(void) { + if (iso_cacheInited) return; + + iso_cacheInited = true; + PackedCol_GetShaded(PACKEDCOL_WHITE, + &iso_colorXSide, &iso_colorZSide, &iso_colorYBottom); +} + +static TextureLoc IsometricDrawer_GetTexLoc(BlockID block, Face face) { + TextureLoc loc = Block_Tex(block, face); + *iso_state++ = Atlas1D_Index(loc); + return loc; +} + +static void IsometricDrawer_Flat(BlockID block, float size) { + int texIndex; + TextureLoc loc = Block_Tex(block, FACE_ZMAX); + TextureRec rec = Atlas1D_TexRec(loc, 1, &texIndex); + + struct VertexTextured v; + float minX, maxX, minY, maxY; + float scale; + + *iso_state++ = texIndex; + v.Col = PACKEDCOL_WHITE; + Block_Tint(v.Col, block); + + /* Rescale by 0.70 in Classic mode to match vanilla size */ + /* Rescale by 0.88 in Enhanced mode to be slightly nicer */ + /* Default selected size: 54px -> 48px */ + /* Default inventory size: 36px -> 32px */ + /* Default hotbar size: 28px -> 24px */ + scale = Game_ClassicMode ? 0.70f : 0.88f; + size = Math_Ceil(size * scale); + minX = iso_posX - size; maxX = iso_posX + size; + minY = iso_posY - size; maxY = iso_posY + size; + + v.z = 0.0f; + v.x = minX; v.y = minY; v.U = rec.u1; v.V = rec.v1; *iso_vertices++ = v; + v.y = maxY; v.V = rec.v2; *iso_vertices++ = v; + v.x = maxX; v.U = rec.u2; *iso_vertices++ = v; + v.y = minY; v.V = rec.v1; *iso_vertices++ = v; +} + +static void IsometricDrawer_Angled(BlockID block, float size) { + cc_bool bright; + Vec3 min, max; + struct VertexTextured* beg = iso_vertices; + struct VertexTextured* v; + float x, y, scale; + + /* isometric coords size: cosY * -scale - sinY * scale */ + /* we need to divide by (2 * cosY), as the calling function expects size to be in pixels. */ + scale = size / (2.0f * iso_cosY); + + Drawer.MinBB = Blocks.MinBB[block]; Drawer.MinBB.y = 1.0f - Drawer.MinBB.y; + Drawer.MaxBB = Blocks.MaxBB[block]; Drawer.MaxBB.y = 1.0f - Drawer.MaxBB.y; + min = Blocks.MinBB[block]; max = Blocks.MaxBB[block]; + + Drawer.X1 = scale * (1.0f - min.x * 2.0f); + Drawer.X2 = scale * (1.0f - max.x * 2.0f); + Drawer.Y1 = scale * (1.0f - min.y * 2.0f); + Drawer.Y2 = scale * (1.0f - max.y * 2.0f); + Drawer.Z1 = scale * (1.0f - min.z * 2.0f); + Drawer.Z2 = scale * (1.0f - max.z * 2.0f); + + bright = Blocks.Brightness[block]; + Drawer.Tinted = Blocks.Tinted[block]; + Drawer.TintCol = Blocks.FogCol[block]; + + Drawer_XMax(1, bright ? PACKEDCOL_WHITE : iso_colorXSide, + IsometricDrawer_GetTexLoc(block, FACE_XMAX), &iso_vertices); + Drawer_ZMin(1, bright ? PACKEDCOL_WHITE : iso_colorZSide, + IsometricDrawer_GetTexLoc(block, FACE_ZMIN), &iso_vertices); + Drawer_YMax(1, PACKEDCOL_WHITE, + IsometricDrawer_GetTexLoc(block, FACE_YMAX), &iso_vertices); + + for (v = beg; v < iso_vertices; v++) + { + /* Cut down form of: */ + /* Matrix_RotateY(&rotY, 45.0f * MATH_DEG2RAD); */ + /* Matrix_RotateX(&rotX, -30.0f * MATH_DEG2RAD); */ + /* Matrix_Mul(&iso_transform, &rotY, &rotX); */ + /* ... */ + /* Vec3 vec = { v.x, v.y, v.z }; */ + /* Vec3_Transform(&vec, &vec, &iso_transform); */ + /* With all unnecessary operations either simplified or removed */ + x = v->x * iso_cosY + v->z * -iso_sinY; + y = v->x * iso_sinX * iso_sinY + v->y * iso_cosX + v->z * iso_sinX * iso_cosY; + + v->x = x + iso_posX; + v->y = y + iso_posY; + } +} + +void IsometricDrawer_BeginBatch(struct VertexTextured* vertices, int* state) { + IsometricDrawer_InitCache(); + iso_vertices = vertices; + iso_vertices_base = vertices; + iso_state = state; /* TODO just store TextureLoc ??? */ +} + +void IsometricDrawer_AddBatch(BlockID block, float size, float x, float y) { + if (Blocks.Draw[block] == DRAW_GAS) return; + + iso_posX = x; iso_posY = y; + /* See comment in Gfx_Make2DQuad() for why 0.5 is subtracted in D3D9 */ + /* TODO pass as arguments? test diff */ +#if CC_GFX_BACKEND == CC_GFX_BACKEND_D3D9 + iso_posX -= 0.5f; iso_posY -= 0.5f; +#endif + + if (Blocks.Draw[block] == DRAW_SPRITE) { + IsometricDrawer_Flat(block, size); + } else { + IsometricDrawer_Angled(block, size); + } +} + +int IsometricDrawer_EndBatch(void) { + return (int)(iso_vertices - iso_vertices_base); +} + +void IsometricDrawer_Render(int count, int offset, int* state) { + int i, curIdx, batchBeg, batchLen; + + curIdx = state[0]; + batchLen = 0; + batchBeg = offset; + + for (i = 0; i < count / 4; i++, batchLen += 4) + { + if (state[i] == curIdx) continue; + + /* Flush previous batch */ + Gfx_BindTexture(Atlas1D.TexIds[curIdx]); + Gfx_DrawVb_IndexedTris_Range(batchLen, batchBeg); + + /* Reset for next batch */ + curIdx = state[i]; + batchBeg += batchLen; + batchLen = 0; + } + + Gfx_BindTexture(Atlas1D.TexIds[curIdx]); + Gfx_DrawVb_IndexedTris_Range(batchLen, batchBeg); +} diff --git a/src/IsometricDrawer.h b/src/IsometricDrawer.h new file mode 100644 index 0000000..fd20711 --- /dev/null +++ b/src/IsometricDrawer.h @@ -0,0 +1,20 @@ +#ifndef CC_ISOMETRICDRAWER_H +#define CC_ISOMETRICDRAWER_H +#include "Core.h" +/* Draws 2D isometric blocks for the hotbar and inventory UIs. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct VertexTextured; + +/* Maximum number of vertices used to draw a block in isometric way. */ +#define ISOMETRICDRAWER_MAXVERTICES 12 + +/* Sets up state to begin drawing blocks isometrically */ +void IsometricDrawer_BeginBatch(struct VertexTextured* vertices, int* state); +/* Buffers the vertices needed to draw the given block at the given position */ +void IsometricDrawer_AddBatch(BlockID block, float size, float x, float y); +/* Returns the number of buffered vertices */ +int IsometricDrawer_EndBatch(void); +/* Draws the buffered vertices */ +void IsometricDrawer_Render(int count, int offset, int* state); +#endif diff --git a/src/LBackend.c b/src/LBackend.c new file mode 100644 index 0000000..b2c0412 --- /dev/null +++ b/src/LBackend.c @@ -0,0 +1,1179 @@ +#include "LBackend.h" +#if defined CC_BUILD_WEB +/* Web backend doesn't use the launcher */ +#elif defined CC_BUILD_WIN_TEST +/* Testing windows UI backend */ +#include "LBackend_Win.c" +#elif defined CC_BUILD_IOS +/* iOS uses custom UI backend */ +#else +#include "Launcher.h" +#include "Drawer2D.h" +#include "Window.h" +#include "LWidgets.h" +#include "String.h" +#include "Gui.h" +#include "Drawer2D.h" +#include "Launcher.h" +#include "ExtMath.h" +#include "Window.h" +#include "Funcs.h" +#include "LWeb.h" +#include "Platform.h" +#include "LScreens.h" +#include "Input.h" +#include "Utils.h" +#include "Event.h" +#include "Stream.h" +#include "Logger.h" +#include "Errors.h" + +struct FontDesc titleFont, textFont, hintFont, logoFont, rowFont; +/* Contains the pixels that are drawn to the window */ +static struct Context2D framebuffer; +/* The area/region of the window that needs to be redrawn and presented to the screen. */ +/* If width is 0, means no area needs to be redrawn. */ +Rect2D dirty_rect; + +static cc_uint8 pendingRedraw; +#define REDRAW_ALL 0x02 +#define REDRAW_SOME 0x01 + +static int xBorder, xBorder2, xBorder3, xBorder4; +static int yBorder, yBorder2, yBorder3, yBorder4; +static int xInputOffset, yInputOffset, inputExpand; +static int caretOffset, caretWidth, caretHeight; +static int scrollbarWidth, dragPad, gridlineWidth, gridlineHeight; +static int hdrYOffset, hdrYPadding, rowYOffset, rowYPadding; +static int cellXOffset, cellXPadding, cellMinWidth; +static int flagXOffset, flagYOffset; + +static void HookEvents(void); +void LBackend_Init(void) { + xBorder = Display_ScaleX(1); + yBorder = Display_ScaleY(1); + + if (xBorder < 1) { xBorder = 1; } + if (yBorder < 1) { yBorder = 1; } + + xBorder2 = xBorder * 2; xBorder3 = xBorder * 3; xBorder4 = xBorder * 4; + yBorder2 = yBorder * 2; yBorder3 = yBorder * 3; yBorder4 = yBorder * 4; + + xInputOffset = Display_ScaleX(5); + yInputOffset = Display_ScaleY(2); + inputExpand = Display_ScaleX(20); + + caretOffset = Display_ScaleY(5); + caretWidth = Display_ScaleX(10); + caretHeight = Display_ScaleY(2); + + scrollbarWidth = Display_ScaleX(10); + dragPad = Display_ScaleX(8); + gridlineWidth = Display_ScaleX(2); + gridlineHeight = Display_ScaleY(2); + + hdrYOffset = Display_ScaleY(3); + hdrYPadding = Display_ScaleY(5); + rowYOffset = Display_ScaleY(3); + rowYPadding = Display_ScaleY(1); + + cellXOffset = Display_ScaleX(6); + cellXPadding = Display_ScaleX(5); + cellMinWidth = Display_ScaleX(20); + flagXOffset = Display_ScaleX(2); + flagYOffset = Display_ScaleY(6); + + Font_Make(&titleFont, 16, FONT_FLAGS_BOLD); + Font_Make(&textFont, 14, FONT_FLAGS_NONE); + Font_Make(&hintFont, 12, FONT_FLAGS_NONE); + HookEvents(); +} + +void LBackend_Free(void) { + Font_Free(&titleFont); + Font_Free(&textFont); + Font_Free(&hintFont); + Font_Free(&logoFont); + Font_Free(&rowFont); +} + +void LBackend_UpdateTitleFont(void) { + Font_Free(&logoFont); + Launcher_MakeTitleFont(&logoFont); +} +void LBackend_DrawTitle(struct Context2D* ctx, const char* title) { + Launcher_DrawTitle(&logoFont, title, ctx); +} + +/* Scales up flag bitmap if necessary */ +static void LBackend_ScaleFlag(struct Bitmap* bmp) { + struct Bitmap scaled; + int width = Display_ScaleX(bmp->width); + int height = Display_ScaleY(bmp->height); + /* at default DPI don't need to rescale it */ + if (width == bmp->width && height == bmp->height) return; + + Bitmap_TryAllocate(&scaled, width, height); + if (!scaled.scan0) { + Logger_SysWarn(ERR_OUT_OF_MEMORY, "resizing flags bitmap"); return; + } + + Bitmap_Scale(&scaled, bmp, 0, 0, bmp->width, bmp->height); + Mem_Free(bmp->scan0); + *bmp = scaled; +} + +void LBackend_DecodeFlag(struct Flag* flag, cc_uint8* data, cc_uint32 len) { + struct Stream s; + cc_result res; + + Stream_ReadonlyMemory(&s, data, len); + res = Png_Decode(&flag->bmp, &s); + if (res) Logger_SysWarn(res, "decoding flag"); + flag->meta = NULL; + + LBackend_ScaleFlag(&flag->bmp); +} + +static void OnPointerMove(void* obj, int idx); +void LBackend_SetScreen(struct LScreen* s) { + int i; + /* for hovering over active button etc */ + for (i = 0; i < Pointers_Count; i++) { + OnPointerMove(s, i); + } +} + +void LBackend_CloseScreen(struct LScreen* s) { } + +static void LBackend_LayoutDimensions(struct LWidget* w) { + const struct LLayout* l = w->layouts + 2; + while (l->type) + { + switch (l->type) + { + case LLAYOUT_WIDTH: + w->width = Window_Main.Width - w->x - Display_ScaleX(l->offset); + w->width = max(1, w->width); + break; + case LLAYOUT_HEIGHT: + w->height = Window_Main.Height - w->y - Display_ScaleY(l->offset); + w->height = max(1, w->height); + break; + } + l++; + } +} + +void LBackend_LayoutWidget(struct LWidget* w) { + const struct LLayout* l = w->layouts; + + w->x = Gui_CalcPos(l[0].type & 0xFF, Display_ScaleX(l[0].offset), w->width, Window_Main.Width); + w->y = Gui_CalcPos(l[1].type & 0xFF, Display_ScaleY(l[1].offset), w->height, Window_Main.Height); + + /* e.g. Table widget needs adjusts width/height based on window */ + if (l[1].type & LLAYOUT_EXTRA) + LBackend_LayoutDimensions(w); + + if (w->type != LWIDGET_TABLE) return; + LBackend_TableReposition((struct LTable*)w); +} + +void LBackend_MarkDirty(void* widget) { + struct LWidget* w = (struct LWidget*)widget; + pendingRedraw |= REDRAW_SOME; + w->dirty = true; +} + +/* Marks the entire window as needing to be redrawn. */ +static CC_NOINLINE void MarkAllDirty(void) { + dirty_rect.x = 0; dirty_rect.width = framebuffer.width; + dirty_rect.y = 0; dirty_rect.height = framebuffer.height; +} + +/* Marks the given area/region as needing to be redrawn. */ +static CC_NOINLINE void MarkAreaDirty(int x, int y, int width, int height) { + int x1, y1, x2, y2; + if (!Drawer2D_Clamp(&framebuffer, &x, &y, &width, &height)) return; + + /* union with existing dirty area */ + if (dirty_rect.width) { + x1 = min(x, dirty_rect.x); + y1 = min(y, dirty_rect.y); + + x2 = max(x + width, dirty_rect.x + dirty_rect.width); + y2 = max(y + height, dirty_rect.y + dirty_rect.height); + + x = x1; width = x2 - x1; + y = y1; height = y2 - y1; + } + + dirty_rect.x = x; dirty_rect.width = width; + dirty_rect.y = y; dirty_rect.height = height; +} + +void LBackend_InitFramebuffer(void) { + struct Bitmap bmp; + int width = max(Window_Main.Width, 1); + int height = max(Window_Main.Height, 1); + + Window_AllocFramebuffer(&bmp, width, height); + Context2D_Wrap(&framebuffer, &bmp); + /* Backing surface may be bigger then valid area */ + framebuffer.width = width; + framebuffer.height = height; +} + +void LBackend_FreeFramebuffer(void) { + Window_FreeFramebuffer(&framebuffer.bmp); +} + + +/*########################################################################################################################* +*------------------------------------------------------Base drawing-------------------------------------------------------* +*#########################################################################################################################*/ +static void DrawBoxBounds(BitmapCol color, int x, int y, int width, int height) { + Context2D_Clear(&framebuffer, color, + x, y, + width, yBorder); + Context2D_Clear(&framebuffer, color, + x, y + height - yBorder, + width, yBorder); + Context2D_Clear(&framebuffer, color, + x, y, + xBorder, height); + Context2D_Clear(&framebuffer, color, + x + width - xBorder, y, + xBorder, height); +} + +static CC_NOINLINE void DrawWidget(struct LWidget* w) { + w->last.x = w->x; w->last.width = w->width; + w->last.y = w->y; w->last.height = w->height; + + w->dirty = false; + w->VTABLE->Draw(w); + MarkAreaDirty(w->x, w->y, w->width, w->height); +} + +static CC_NOINLINE void RedrawAll(void) { + struct LScreen* s = Launcher_Active; + int i; + s->DrawBackground(s, &framebuffer); + + for (i = 0; i < s->numWidgets; i++) { + DrawWidget(s->widgets[i]); + } + MarkAllDirty(); +} + +static CC_NOINLINE void RedrawDirty(void) { + struct LScreen* s = Launcher_Active; + struct LWidget* w; + int i; + + for (i = 0; i < s->numWidgets; i++) { + w = s->widgets[i]; + if (!w->dirty) continue; + + /* check if widget might need redrawing of background behind */ + if (!w->opaque || w->last.width > w->width || w->last.height > w->height) { + s->ResetArea(&framebuffer, + w->last.x, w->last.y, w->last.width, w->last.height); + MarkAreaDirty(w->last.x, w->last.y, w->last.width, w->last.height); + } + DrawWidget(w); + } +} + +static CC_NOINLINE void DoRedraw(void) { + if (pendingRedraw & REDRAW_ALL) { + RedrawAll(); + pendingRedraw = 0; + } else if (pendingRedraw & REDRAW_SOME) { + RedrawDirty(); + pendingRedraw = 0; + } +} + +void LBackend_Redraw(void) { + pendingRedraw = REDRAW_ALL; + MarkAllDirty(); +} +void LBackend_ThemeChanged(void) { LBackend_Redraw(); } + +void LBackend_Tick(void) { + DoRedraw(); + if (!dirty_rect.width) return; + + OnscreenKeyboard_Draw2D(&dirty_rect, &framebuffer.bmp); + Window_DrawFramebuffer(dirty_rect, &framebuffer.bmp); + + dirty_rect.x = 0; dirty_rect.width = 0; + dirty_rect.y = 0; dirty_rect.height = 0; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Event handling------------------------------------------------------* +*#########################################################################################################################*/ +static void ReqeustRedraw(void* obj) { LBackend_Redraw(); } +static void RedrawContents(void* obj) { DoRedraw(); } + +CC_NOINLINE static struct LWidget* GetWidgetAt(struct LScreen* s, int idx) { + struct LWidget* w; + int i, x = Pointers[idx].x, y = Pointers[idx].y; + + for (i = 0; i < s->numWidgets; i++) { + w = s->widgets[i]; + if (Gui_Contains(w->x, w->y, w->width, w->height, x, y)) return w; + } + return NULL; +} + +static void OnPointerDown(void* obj, int idx) { + struct LScreen* s = Launcher_Active; + struct LWidget* over; + struct LWidget* prev; + if (Window_Main.SoftKeyboardFocus) return; + + if (!s) return; + over = GetWidgetAt(s, idx); + prev = s->selectedWidget; + + if (prev && over != prev) LScreen_UnselectWidget(s, idx, prev); + if (over) LScreen_SelectWidget(s, idx, over, over == prev); +} + +static void OnPointerUp(void* obj, int idx) { + struct LScreen* s = Launcher_Active; + struct LWidget* over; + struct LWidget* prev; + if (Window_Main.SoftKeyboardFocus) return; + + if (!s) return; + over = GetWidgetAt(s, idx); + prev = s->selectedWidget; + + /* if user moves mouse away, it doesn't count */ + if (over != prev) { + LScreen_UnselectWidget(s, idx, prev); + } else if (over && over->OnClick) { + over->OnClick(over); + } + /* TODO eliminate this hack */ + s->MouseUp(s, idx); +} + +static void OnPointerMove(void* obj, int idx) { + struct LScreen* s = Launcher_Active; + struct LWidget* over; + struct LWidget* prev; + cc_bool overSame; + if (Window_Main.SoftKeyboardFocus) return; + + if (!s) return; + over = GetWidgetAt(s, idx); + prev = s->hoveredWidget; + overSame = prev == over; + + if (prev && !overSame) { + prev->hovered = false; + s->hoveredWidget = NULL; + + if (prev->OnUnhover) prev->OnUnhover(prev); + if (prev->VTABLE->MouseLeft) prev->VTABLE->MouseLeft(prev); + } + + if (over) { + over->hovered = true; + s->hoveredWidget = over; + + if (over->OnHover) over->OnHover(over); + if (!over->VTABLE->MouseMove) return; + over->VTABLE->MouseMove(over, idx, overSame); + } +} + +static void OnKeyPress(void* obj, int cp) { + struct LWidget* selected; + char c; + if (!Convert_TryCodepointToCP437(cp, &c)) return; + + selected = Launcher_Active->selectedWidget; + if (!selected) return; + + if (!selected->VTABLE->KeyPress) return; + selected->VTABLE->KeyPress(selected, c); +} + +static void OnTextChanged(void* obj, const cc_string* str) { + struct LWidget* selected = Launcher_Active->selectedWidget; + if (!selected) return; + + if (!selected->VTABLE->TextChanged) return; + selected->VTABLE->TextChanged(selected, str); +} + +static void HookEvents(void) { + Event_Register_(&PointerEvents.Down, NULL, OnPointerDown); + Event_Register_(&PointerEvents.Up, NULL, OnPointerUp); + Event_Register_(&PointerEvents.Moved, NULL, OnPointerMove); + + Event_Register_(&InputEvents.Press, NULL, OnKeyPress); + Event_Register_(&InputEvents.TextChanged, NULL, OnTextChanged); + + Event_Register_(&WindowEvents.RedrawNeeded, NULL, ReqeustRedraw); + Event_Register_(&WindowEvents.Redrawing, NULL, RedrawContents); +} + + +/*########################################################################################################################* +*------------------------------------------------------ButtonWidget-------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_ButtonInit(struct LButton* w, int width, int height) { + w->width = Display_ScaleX(width); + w->height = Display_ScaleY(height); +} + +void LBackend_ButtonUpdate(struct LButton* w) { + struct DrawTextArgs args; + DrawTextArgs_Make(&args, &w->text, &titleFont, true); + LBackend_MarkDirty(w); + + w->_textWidth = Drawer2D_TextWidth(&args); + w->_textHeight = Drawer2D_TextHeight(&args); +} + +void LBackend_ButtonDraw(struct LButton* w) { + struct DrawTextArgs args; + int xOffset, yOffset; + cc_bool active = w->hovered || w->selected; + + LButton_DrawBackground(&framebuffer, w->x, w->y, w->width, w->height, active); + xOffset = w->width - w->_textWidth; + yOffset = w->height - w->_textHeight; + DrawTextArgs_Make(&args, &w->text, &titleFont, true); + + if (!active) Drawer2D.Colors['f'] = Drawer2D.Colors['7']; + Context2D_DrawText(&framebuffer, &args, + w->x + xOffset / 2, w->y + yOffset / 2); + + if (!active) Drawer2D.Colors['f'] = Drawer2D.Colors['F']; +} + + +/*########################################################################################################################* +*-----------------------------------------------------CheckboxWidget------------------------------------------------------* +*#########################################################################################################################*/ +#define CB_SIZE 24 +#define CB_OFFSET 8 + +static void LCheckbox_OnClick(void* w) { + struct LCheckbox* cb = (struct LCheckbox*)w; + LBackend_MarkDirty(cb); + + cb->value = !cb->value; + if (cb->ValueChanged) cb->ValueChanged(cb); +} + +void LBackend_CheckboxInit(struct LCheckbox* w) { + struct DrawTextArgs args; + DrawTextArgs_Make(&args, &w->text, &textFont, true); + + w->width = Display_ScaleX(CB_SIZE + CB_OFFSET) + Drawer2D_TextWidth(&args); + w->height = Display_ScaleY(CB_SIZE); + w->OnClick = LCheckbox_OnClick; +} + +void LBackend_CheckboxUpdate(struct LCheckbox* w) { + LBackend_MarkDirty(w); +} + +/* Based off checkbox from original ClassiCube Launcher */ +static const cc_uint8 checkbox_indices[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x06, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x06, 0x09, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x06, 0x0B, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0D, 0x0E, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0F, 0x06, 0x10, 0x00, 0x11, 0x06, 0x12, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x13, 0x14, 0x15, 0x00, 0x16, 0x17, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x06, 0x19, 0x06, 0x1A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1B, 0x06, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1D, 0x06, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; +static const BitmapCol checkbox_palette[] = { + BitmapCol_Make(0,0,0,0), BitmapColor_RGB(144, 144, 144), + BitmapColor_RGB( 61, 61, 61), BitmapColor_RGB( 94, 94, 94), + BitmapColor_RGB(197, 196, 197), BitmapColor_RGB( 57, 57, 57), + BitmapColor_RGB( 33, 33, 33), BitmapColor_RGB(177, 177, 177), + BitmapColor_RGB(189, 189, 189), BitmapColor_RGB( 67, 67, 67), + BitmapColor_RGB(108, 108, 108), BitmapColor_RGB(171, 171, 171), + BitmapColor_RGB(220, 220, 220), BitmapColor_RGB( 43, 43, 43), + BitmapColor_RGB( 63, 63, 63), BitmapColor_RGB(100, 100, 100), + BitmapColor_RGB(192, 192, 192), BitmapColor_RGB(132, 132, 132), + BitmapColor_RGB(175, 175, 175), BitmapColor_RGB(217, 217, 217), + BitmapColor_RGB( 42, 42, 42), BitmapColor_RGB( 86, 86, 86), + BitmapColor_RGB( 56, 56, 56), BitmapColor_RGB( 76, 76, 76), + BitmapColor_RGB(139, 139, 139), BitmapColor_RGB(130, 130, 130), + BitmapColor_RGB(181, 181, 181), BitmapColor_RGB( 62, 62, 62), + BitmapColor_RGB( 75, 75, 75), BitmapColor_RGB(184, 184, 184), +}; + +static void DrawIndexed(int size, int x, int y, struct Context2D* ctx) { + struct Bitmap* bmp = (struct Bitmap*)ctx; + BitmapCol* row, color; + int i, xx, yy; + + for (i = 0, yy = 0; yy < size; yy++) { + if ((y + yy) < 0) { i += size; continue; } + if ((y + yy) >= bmp->height) break; + row = Bitmap_GetRow(bmp, y + yy); + + for (xx = 0; xx < size; xx++) { + color = checkbox_palette[checkbox_indices[i++]]; + if (color == 0) continue; /* transparent pixel */ + + if ((x + xx) < 0 || (x + xx) >= bmp->width) continue; + row[x + xx] = color; + } + } +} + +void LBackend_CheckboxDraw(struct LCheckbox* w) { + BitmapCol boxTop = BitmapColor_RGB(255, 255, 255); + BitmapCol boxBottom = BitmapColor_RGB(240, 240, 240); + struct DrawTextArgs args; + int x, y, width, height; + + width = Display_ScaleX(CB_SIZE); + height = Display_ScaleY(CB_SIZE); + + Gradient_Vertical(&framebuffer, boxTop, boxBottom, + w->x, w->y, width, height / 2); + Gradient_Vertical(&framebuffer, boxBottom, boxTop, + w->x, w->y + height / 2, width, height / 2); + + if (w->value) { + const int size = 12; + x = w->x + width / 2 - size / 2; + y = w->y + height / 2 - size / 2; + DrawIndexed(size, x, y, &framebuffer); + } + DrawBoxBounds(BITMAPCOLOR_BLACK, w->x, w->y, width, height); + + DrawTextArgs_Make(&args, &w->text, &textFont, true); + x = w->x + Display_ScaleX(CB_SIZE + CB_OFFSET); + y = w->y + (height - Drawer2D_TextHeight(&args)) / 2; + Context2D_DrawText(&framebuffer, &args, x, y); +} + + +/*########################################################################################################################* +*------------------------------------------------------InputWidget--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint64 caretStart; +static Rect2D caretRect, lastCaretRect; +#define Rect2D_Equals(a, b) a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height + +void LBackend_InputInit(struct LInput* w, int width) { + w->width = Display_ScaleX(width); + w->height = Display_ScaleY(LINPUT_HEIGHT); + w->minWidth = w->width; + + /* Text may end up being wider than minimum width */ + if (w->text.length) LBackend_InputUpdate(w); +} + +void LBackend_InputUpdate(struct LInput* w) { + cc_string text; char textBuffer[STRING_SIZE]; + struct DrawTextArgs args; + int textWidth; + + String_InitArray(text, textBuffer); + LInput_UNSAFE_GetText(w, &text); + LBackend_MarkDirty(w); + + DrawTextArgs_Make(&args, &text, &textFont, false); + textWidth = Drawer2D_TextWidth(&args); + w->width = max(w->minWidth, textWidth + inputExpand); + w->_textHeight = Drawer2D_TextHeight(&args); +} + +static Rect2D LInput_MeasureCaret(struct LInput* w, cc_string* text) { + struct DrawTextArgs args; + Rect2D r; + DrawTextArgs_Make(&args, text, &textFont, true); + + r.x = w->x + xInputOffset; + r.y = w->y + w->height - caretOffset; r.height = caretHeight; + + if (w->caretPos == -1) { + r.x += Drawer2D_TextWidth(&args); + r.width = caretWidth; + } else { + args.text = String_UNSAFE_Substring(text, 0, w->caretPos); + r.x += Drawer2D_TextWidth(&args); + + args.text = String_UNSAFE_Substring(text, w->caretPos, 1); + r.width = Drawer2D_TextWidth(&args); + } + return r; +} + +static void LInput_MoveCaretToCursor(struct LInput* w, int idx) { + cc_string text; char textBuffer[STRING_SIZE]; + int x = Pointers[idx].x, y = Pointers[idx].y; + struct DrawTextArgs args; + int i, charX, charWidth; + + /* Input widget may have been selected by pressing tab */ + /* In which case cursor is completely outside, so ignore */ + if (!Gui_Contains(w->x, w->y, w->width, w->height, x, y)) return; + + String_InitArray(text, textBuffer); + LInput_UNSAFE_GetText(w, &text); + x -= w->x; y -= w->y; + + DrawTextArgs_Make(&args, &text, &textFont, true); + if (x >= Drawer2D_TextWidth(&args)) { + w->caretPos = -1; return; + } + + for (i = 0; i < text.length; i++) { + args.text = String_UNSAFE_Substring(&text, 0, i); + charX = Drawer2D_TextWidth(&args); + + args.text = String_UNSAFE_Substring(&text, i, 1); + charWidth = Drawer2D_TextWidth(&args); + if (x >= charX && x < charX + charWidth) { + w->caretPos = i; return; + } + } +} + +void LBackend_InputTick(struct LInput* w) { + int elapsed; + cc_bool caretShow; + Rect2D r; + + if (!caretStart) return; + elapsed = Stopwatch_ElapsedMS(caretStart, Stopwatch_Measure()); + + caretShow = (elapsed % 1000) < 500; + if (caretShow == w->caretShow) return; + w->caretShow = caretShow; + + LBackend_InputDraw(w); + r = caretRect; + + if (Rect2D_Equals(r, lastCaretRect)) { + /* Fast path, caret is blinking in same spot */ + MarkAreaDirty(r.x, r.y, r.width, r.height); + } else { + /* Slow path (new widget, caret moved, etc) */ + MarkAreaDirty(w->x, w->y, w->width, w->height); + } + lastCaretRect = r; +} + +void LBackend_InputSelect(struct LInput* w, int idx, cc_bool wasSelected) { + struct OpenKeyboardArgs args; + caretStart = Stopwatch_Measure(); + w->caretShow = true; + LInput_MoveCaretToCursor(w, idx); + LBackend_MarkDirty(w); + + if (wasSelected) return; + OpenKeyboardArgs_Init(&args, &w->text, w->inputType); + OnscreenKeyboard_Open(&args); +} + +void LBackend_InputUnselect(struct LInput* w) { + caretStart = 0; + w->caretShow = false; + LBackend_MarkDirty(w); + OnscreenKeyboard_Close(); +} + + +static void LInput_DrawOuterBorder(struct LInput* w) { + struct LScreen* s = Launcher_Active; + struct Context2D* ctx = &framebuffer; + BitmapCol color = Launcher_Theme.ButtonBorderColor; + + if (w->selected) { + DrawBoxBounds(color, w->x, w->y, w->width, w->height); + } else { + s->ResetArea(ctx, w->x, w->y, + w->width, yBorder); + s->ResetArea(ctx, w->x, w->y + w->height - yBorder, + w->width, yBorder); + s->ResetArea(ctx, w->x, w->y, + xBorder, w->height); + s->ResetArea(ctx, w->x + w->width - xBorder, w->y, + xBorder, w->height); + } +} + +static void LInput_DrawInnerBorder(struct LInput* w) { + /* e.g. for modern theme: 162,131,186 --> 165,142,168 */ + BitmapCol color = BitmapColor_Offset(Launcher_Theme.ButtonHighlightColor, 3,11,-18); + + Context2D_Clear(&framebuffer, color, + w->x + xBorder, w->y + yBorder, + w->width - xBorder2, yBorder); + Context2D_Clear(&framebuffer, color, + w->x + xBorder, w->y + w->height - yBorder2, + w->width - xBorder2, yBorder); + Context2D_Clear(&framebuffer, color, + w->x + xBorder, w->y + yBorder, + xBorder, w->height - yBorder2); + Context2D_Clear(&framebuffer, color, + w->x + w->width - xBorder2, w->y + yBorder, + xBorder, w->height - yBorder2); +} + +static void LInput_BlendBoxTop(struct LInput* w) { + BitmapCol color = BitmapColor_RGB(0, 0, 0); + + Gradient_Blend(&framebuffer, color, 75, + w->x + xBorder, w->y + yBorder, + w->width - xBorder2, yBorder); + Gradient_Blend(&framebuffer, color, 50, + w->x + xBorder, w->y + yBorder2, + w->width - xBorder2, yBorder); + Gradient_Blend(&framebuffer, color, 25, + w->x + xBorder, w->y + yBorder3, + w->width - xBorder2, yBorder); +} + +static void LInput_DrawText(struct LInput* w, struct DrawTextArgs* args) { + int y, hintHeight; + + if (w->text.length || !w->hintText) { + y = w->y + (w->height - w->_textHeight) / 2; + Context2D_DrawText(&framebuffer, args, + w->x + xInputOffset, y + yInputOffset); + } else { + args->text = String_FromReadonly(w->hintText); + args->font = &hintFont; + + hintHeight = Drawer2D_TextHeight(args); + y = w->y + (w->height - hintHeight) / 2; + + Drawer2D.Colors['f'] = BitmapColor_RGB(125, 125, 125); + Context2D_DrawText(&framebuffer, args, + w->x + xInputOffset, y); + Drawer2D.Colors['f'] = BITMAPCOLOR_WHITE; + } +} + +void LBackend_InputDraw(struct LInput* w) { + cc_string text; char textBuffer[STRING_SIZE]; + struct DrawTextArgs args; + + String_InitArray(text, textBuffer); + LInput_UNSAFE_GetText(w, &text); + DrawTextArgs_Make(&args, &text, &textFont, false); + + LInput_DrawOuterBorder(w); + LInput_DrawInnerBorder(w); + Context2D_Clear(&framebuffer, BITMAPCOLOR_WHITE, + w->x + xBorder2, w->y + yBorder2, + w->width - xBorder4, w->height - yBorder4); + LInput_BlendBoxTop(w); + + Drawer2D.Colors['f'] = Drawer2D.Colors['0']; + LInput_DrawText(w, &args); + Drawer2D.Colors['f'] = Drawer2D.Colors['F']; + + caretRect = LInput_MeasureCaret(w, &text); + if (!w->caretShow) return; + Context2D_Clear(&framebuffer, BITMAPCOLOR_BLACK, + caretRect.x, caretRect.y, caretRect.width, caretRect.height); +} + + +/*########################################################################################################################* +*------------------------------------------------------LabelWidget--------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_LabelInit(struct LLabel* w) { } +#define LLabel_GetFont(w) (w->small ? &hintFont : &textFont) + +void LBackend_LabelUpdate(struct LLabel* w) { + struct DrawTextArgs args; + DrawTextArgs_Make(&args, &w->text, LLabel_GetFont(w), true); + LBackend_MarkDirty(w); + + w->width = Drawer2D_TextWidth(&args); + w->height = Drawer2D_TextHeight(&args); +} + +void LBackend_LabelDraw(struct LLabel* w) { + struct DrawTextArgs args; + DrawTextArgs_Make(&args, &w->text, LLabel_GetFont(w), true); + Context2D_DrawText(&framebuffer, &args, w->x, w->y); +} + + +/*########################################################################################################################* +*-------------------------------------------------------LineWidget--------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_LineInit(struct LLine* w, int width) { + w->width = Display_ScaleX(width); + w->height = Display_ScaleY(LLINE_HEIGHT); +} + +void LBackend_LineDraw(struct LLine* w) { + BitmapCol color = LLine_GetColor(); + Gradient_Blend(&framebuffer, color, 128, w->x, w->y, w->width, w->height); +} + + +/*########################################################################################################################* +*------------------------------------------------------SliderWidget-------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_SliderInit(struct LSlider* w, int width, int height) { + w->width = Display_ScaleX(width); + w->height = Display_ScaleY(height); +} + +void LBackend_SliderUpdate(struct LSlider* w) { + LBackend_MarkDirty(w); +} + +static void LSlider_DrawBoxBounds(struct LSlider* w) { + BitmapCol boundsTop = BitmapColor_RGB(119, 100, 132); + BitmapCol boundsBottom = BitmapColor_RGB(150, 130, 165); + + /* TODO: Check these are actually right */ + Context2D_Clear(&framebuffer, boundsTop, + w->x, w->y, + w->width, yBorder); + Context2D_Clear(&framebuffer, boundsBottom, + w->x, w->y + w->height - yBorder, + w->width, yBorder); + + Gradient_Vertical(&framebuffer, boundsTop, boundsBottom, + w->x, w->y, + xBorder, w->height); + Gradient_Vertical(&framebuffer, boundsTop, boundsBottom, + w->x + w->width - xBorder, w->y, + xBorder, w->height); +} + +static void LSlider_DrawBox(struct LSlider* w) { + BitmapCol progTop = BitmapColor_RGB(220, 204, 233); + BitmapCol progBottom = BitmapColor_RGB(207, 181, 216); + int halfHeight = (w->height - yBorder2) / 2; + + Gradient_Vertical(&framebuffer, progTop, progBottom, + w->x + xBorder, w->y + yBorder, + w->width - xBorder2, halfHeight); + Gradient_Vertical(&framebuffer, progBottom, progTop, + w->x + xBorder, w->y + yBorder + halfHeight, + w->width - xBorder2, halfHeight); +} + +#define LSLIDER_MAXVALUE 100 +void LBackend_SliderDraw(struct LSlider* w) { + int curWidth; + LSlider_DrawBoxBounds(w); + LSlider_DrawBox(w); + + curWidth = (int)((w->width - xBorder2) * w->value / LSLIDER_MAXVALUE); + Context2D_Clear(&framebuffer, w->color, + w->x + xBorder, w->y + yBorder, + curWidth, w->height - yBorder2); +} + + +/*########################################################################################################################* +*-------------------------------------------------------TableWidget-------------------------------------------------------* +*#########################################################################################################################*/ +static void InitRowFont(void) { + if (rowFont.handle) return; + Font_Make(&rowFont, 11, FONT_FLAGS_NONE); +} + +void LBackend_TableInit(struct LTable* w) { } +void LBackend_TableUpdate(struct LTable* w) { } + +void LBackend_TableReposition(struct LTable* w) { + int rowsHeight; + InitRowFont(); + w->hdrHeight = Font_CalcHeight(&textFont, true) + hdrYPadding; + w->rowHeight = Font_CalcHeight(&rowFont, true) + rowYPadding; + + w->rowsBegY = w->y + w->hdrHeight + gridlineHeight; + w->rowsEndY = w->y + w->height; + rowsHeight = w->height - (w->rowsBegY - w->y); + + w->visibleRows = rowsHeight / w->rowHeight; + LTable_ClampTopRow(w); +} + +void LBackend_TableFlagAdded(struct LTable* w) { + /* TODO: Only redraw flags */ + LBackend_MarkDirty(w); +} + +/* Draws background behind column headers */ +static void LTable_DrawHeaderBackground(struct LTable* w) { + BitmapCol gridColor = BitmapColor_RGB(20, 20, 10); + + if (!Launcher_Theme.ClassicBackground) { + Context2D_Clear(&framebuffer, gridColor, + w->x, w->y, w->width, w->hdrHeight); + } else { + Launcher_DrawBackground(&framebuffer, + w->x, w->y, w->width, w->hdrHeight); + } +} + +static BitmapCol LBackend_TableRowColor(struct LTable* w, int row) { + struct ServerInfo* entry = row < w->rowsCount ? LTable_Get(row) : NULL; + cc_bool selected = entry && String_Equals(&entry->hash, w->selectedHash); + cc_bool featured = entry && entry->featured; + + return LTable_RowColor(row, selected, featured); +} + +/* Draws background behind each row in the table */ +static void LTable_DrawRowsBackground(struct LTable* w) { + int y, height, row; + BitmapCol color; + + y = w->rowsBegY; + for (row = w->topRow; ; row++, y += w->rowHeight) { + color = LBackend_TableRowColor(w, row); + + /* last row may get chopped off */ + height = min(y + w->rowHeight, w->rowsEndY) - y; + /* hit the end of the table */ + if (height < 0) break; + + if (color) { + Context2D_Clear(&framebuffer, color, + w->x, y, w->width, height); + } else { + Launcher_DrawBackground(&framebuffer, + w->x, y, w->width, height); + } + } +} + +/* Draws a gridline below column headers and gridlines after each column */ +static void LTable_DrawGridlines(struct LTable* w) { + int i, x; + if (Launcher_Theme.ClassicBackground) return; + + x = w->x; + Context2D_Clear(&framebuffer, Launcher_Theme.BackgroundColor, + x, w->y + w->hdrHeight, w->width, gridlineHeight); + + for (i = 0; i < w->numColumns; i++) { + x += w->columns[i].width; + if (!w->columns[i].hasGridline) continue; + + Context2D_Clear(&framebuffer, Launcher_Theme.BackgroundColor, + x, w->y, gridlineWidth, w->height); + x += gridlineWidth; + } +} + +/* Draws the entire background of the table */ +static void LTable_DrawBackground(struct LTable* w) { + LTable_DrawHeaderBackground(w); + LTable_DrawRowsBackground(w); + LTable_DrawGridlines(w); +} + +/* Draws title of each column at top of the table */ +static void LTable_DrawHeaders(struct LTable* w) { + struct DrawTextArgs args; + int i, x, y; + + DrawTextArgs_MakeEmpty(&args, &textFont, true); + x = w->x; y = w->y; + + for (i = 0; i < w->numColumns; i++) { + args.text = String_FromReadonly(w->columns[i].name); + Drawer2D_DrawClippedText(&framebuffer, &args, + x + cellXOffset, y + hdrYOffset, + w->columns[i].width - cellXPadding); + + x += w->columns[i].width; + if (w->columns[i].hasGridline) x += gridlineWidth; + } +} + +/* Draws contents of the currently visible rows in the table */ +static void LTable_DrawRows(struct LTable* w) { + cc_string str; char strBuffer[STRING_SIZE]; + struct ServerInfo* entry; + struct DrawTextArgs args; + struct LTableCell cell; + int i, x, y, row, end; + + InitRowFont(); + String_InitArray(str, strBuffer); + DrawTextArgs_Make(&args, &str, &rowFont, true); + cell.table = w; + y = w->rowsBegY; + end = w->topRow + w->visibleRows; + + for (row = w->topRow; row < end; row++, y += w->rowHeight) { + x = w->x; + + if (row >= w->rowsCount) break; + if (y + w->rowHeight > w->rowsEndY) break; + entry = LTable_Get(row); + + for (i = 0; i < w->numColumns; i++) { + args.text = str; cell.x = x; cell.y = y; + cell.width = w->columns[i].width; + w->columns[i].DrawRow(entry, &args, &cell, &framebuffer); + + if (args.text.length) { + Drawer2D_DrawClippedText(&framebuffer, &args, + x + cellXOffset, y + rowYOffset, + cell.width - cellXPadding); + } + + x += w->columns[i].width; + if (w->columns[i].hasGridline) x += gridlineWidth; + } + } +} + +/* Draws scrollbar on the right edge of the table */ +static void LTable_DrawScrollbar(struct LTable* w) { + BitmapCol classicBack = BitmapColor_RGB( 80, 80, 80); + BitmapCol classicScroll = BitmapColor_RGB(160, 160, 160); + BitmapCol backCol = Launcher_Theme.ClassicBackground ? classicBack : Launcher_Theme.ButtonBorderColor; + BitmapCol scrollCol = Launcher_Theme.ClassicBackground ? classicScroll : Launcher_Theme.ButtonForeActiveColor; + + int x, y, height; + x = w->x + w->width - scrollbarWidth; + LTable_GetScrollbarCoords(w, &y, &height); + + Context2D_Clear(&framebuffer, backCol, + x, w->y, scrollbarWidth, w->height); + Context2D_Clear(&framebuffer, scrollCol, + x, w->y + y, scrollbarWidth, height); +} + +void LBackend_TableDraw(struct LTable* w) { + LTable_DrawBackground(w); + LTable_DrawHeaders(w); + LTable_DrawRows(w); + LTable_DrawScrollbar(w); + MarkAllDirty(); +} + + +static void LTable_RowsClick(struct LTable* w, int idx) { + int mouseY = Pointers[idx].y - w->rowsBegY; + int row = w->topRow + mouseY / w->rowHeight; + LTable_RowClick(w, row); +} + +/* Handles clicking on column headers (either resizes a column or sort rows) */ +static void LTable_HeadersClick(struct LTable* w, int idx) { + int x, i, mouseX = Pointers[idx].x; + + for (i = 0, x = w->x; i < w->numColumns; i++) { + /* clicked on gridline, begin dragging */ + if (mouseX >= (x - dragPad) && mouseX < (x + dragPad) && w->columns[i].draggable) { + w->draggingColumn = i - 1; + w->dragXStart = (mouseX - w->x) - w->columns[i - 1].width; + return; + } + + x += w->columns[i].width; + if (w->columns[i].hasGridline) x += gridlineWidth; + } + + for (i = 0, x = w->x; i < w->numColumns; i++) { + if (mouseX >= x && mouseX < (x + w->columns[i].width) && w->columns[i].sortable) { + w->sortingCol = i; + w->columns[i].invertSort = !w->columns[i].invertSort; + LTable_Sort(w); + return; + } + + x += w->columns[i].width; + if (w->columns[i].hasGridline) x += gridlineWidth; + } +} + +/* Handles clicking on the scrollbar on right edge of table */ +static void LTable_ScrollbarClick(struct LTable* w, int idx) { + int y, height, mouseY = Pointers[idx].y - w->y; + LTable_GetScrollbarCoords(w, &y, &height); + + if (mouseY < y) { + w->topRow -= w->visibleRows; + } else if (mouseY >= y + height) { + w->topRow += w->visibleRows; + } else { + w->dragYOffset = mouseY - y; + } + + w->draggingScrollbar = true; + LTable_ClampTopRow(w); +} + +void LBackend_TableMouseDown(struct LTable* w, int idx) { + if (Pointers[idx].x >= Window_Main.Width - scrollbarWidth) { + LTable_ScrollbarClick(w, idx); + w->_lastRow = -1; + } else if (Pointers[idx].y < w->rowsBegY) { + LTable_HeadersClick(w, idx); + w->_lastRow = -1; + } else { + LTable_RowsClick(w, idx); + } + LBackend_MarkDirty(w); +} + +void LBackend_TableMouseMove(struct LTable* w, int idx) { + int x = Pointers[idx].x - w->x, y = Pointers[idx].y - w->y; + int i, col, width, maxW; + + if (w->draggingScrollbar) { + float scale = w->height / (float)w->rowsCount; + int row = (int)((y - w->dragYOffset) / scale); + /* avoid expensive redraw when possible */ + if (w->topRow == row) return; + + w->topRow = row; + LTable_ClampTopRow(w); + LBackend_MarkDirty(w); + } else if (w->draggingColumn >= 0) { + col = w->draggingColumn; + width = x - w->dragXStart; + + /* Ensure this column doesn't expand past right side of table */ + maxW = w->width; + for (i = 0; i < col; i++) maxW -= w->columns[i].width; + + Math_Clamp(width, cellMinWidth, maxW - cellMinWidth); + if (width == w->columns[col].width) return; + w->columns[col].width = width; + LBackend_MarkDirty(w); + } +} + +/* Stops an in-progress dragging of resizing column. */ +void LBackend_TableMouseUp(struct LTable* w, int idx) { + w->draggingColumn = -1; + w->draggingScrollbar = false; + w->dragYOffset = 0; +} +#endif diff --git a/src/LBackend.h b/src/LBackend.h new file mode 100644 index 0000000..1d70fe1 --- /dev/null +++ b/src/LBackend.h @@ -0,0 +1,76 @@ +#ifndef CC_LBACKEND_H +#define CC_LBACKEND_H +#include "Core.h" +/* +Abstracts the gui drawing backend for the Launcher +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Context2D; +struct LScreen; +struct LWidget; +struct LButton; +struct LCheckbox; +struct LInput; +struct LLabel; +struct LLine; +struct LSlider; +struct LTable; +struct Flag; + +void LBackend_Init(void); +void LBackend_Free(void); +void LBackend_SetScreen(struct LScreen* s); +void LBackend_CloseScreen(struct LScreen* s); + +void LBackend_UpdateTitleFont(void); +void LBackend_DrawTitle(struct Context2D* ctx, const char* title); + +void LBackend_DecodeFlag(struct Flag* flag, cc_uint8* data, cc_uint32 len); + +/* Resets pixels to default, then draws widgets of current screen over it */ +void LBackend_Redraw(void); +void LBackend_ThemeChanged(void); +void LBackend_Tick(void); +void LBackend_LayoutWidget(struct LWidget* w); +void LBackend_MarkDirty(void* widget); + +void LBackend_InitFramebuffer(void); +void LBackend_FreeFramebuffer(void); + +void LBackend_ButtonInit(struct LButton* w, int width, int height); +void LBackend_ButtonUpdate(struct LButton* w); +void LBackend_ButtonDraw(struct LButton* w); + +void LBackend_CheckboxInit(struct LCheckbox* w); +void LBackend_CheckboxUpdate(struct LCheckbox* w); +void LBackend_CheckboxDraw(struct LCheckbox* w); + +void LBackend_InputInit(struct LInput* w, int width); +void LBackend_InputUpdate(struct LInput* w); +void LBackend_InputDraw(struct LInput* w); + +void LBackend_InputTick(struct LInput* w); +void LBackend_InputSelect(struct LInput* w, int idx, cc_bool wasSelected); +void LBackend_InputUnselect(struct LInput* w); + +void LBackend_LabelInit(struct LLabel* w); +void LBackend_LabelUpdate(struct LLabel* w); +void LBackend_LabelDraw(struct LLabel* w); + +void LBackend_LineInit(struct LLine* w, int width); +void LBackend_LineDraw(struct LLine* w); + +void LBackend_SliderInit(struct LSlider* w, int width, int height); +void LBackend_SliderUpdate(struct LSlider* w); +void LBackend_SliderDraw(struct LSlider* w); + +void LBackend_TableInit(struct LTable* w); +void LBackend_TableUpdate(struct LTable* w); +/* Adjusts Y position of rows and number of visible rows */ +void LBackend_TableReposition(struct LTable* w); +void LBackend_TableDraw(struct LTable* w); + +void LBackend_TableMouseDown(struct LTable* w, int idx); +void LBackend_TableMouseUp(struct LTable* w, int idx); +void LBackend_TableMouseMove(struct LTable* w, int idx); +#endif diff --git a/src/LBackend_Android.c b/src/LBackend_Android.c new file mode 100644 index 0000000..3ec21f3 --- /dev/null +++ b/src/LBackend_Android.c @@ -0,0 +1,614 @@ +#include "LBackend.h" +#if defined CC_BUILD_ANDROID11111111111 +#include "Launcher.h" +#include "Drawer2D.h" +#include "Window.h" +#include "LWidgets.h" +#include "String.h" +#include "Gui.h" +#include "Drawer2D.h" +#include "Launcher.h" +#include "ExtMath.h" +#include "Window.h" +#include "Funcs.h" +#include "LWeb.h" +#include "Platform.h" +#include "LScreens.h" +#include "Input.h" +#include "Utils.h" +#include "Event.h" +#include +#include +#include +#include + +struct FontDesc logoFont; + +static void HookEvents(void); +static void LBackend_InitHooks(void); +void LBackend_Init(void) { + HookEvents(); + LBackend_InitHooks(); +} + +void LBackend_Free(void) { + Font_Free(&logoFont); +} + +void LBackend_UpdateLogoFont(void) { + Font_Free(&logoFont); + Launcher_MakeLogoFont(&logoFont); +} +void LBackend_DrawLogo(struct Context2D* ctx, const char* title) { + Launcher_DrawLogo(&logoFont, title, ctx); +} + +static void LBackend_LayoutDimensions(struct LWidget* w) { + const struct LLayout* l = w->layouts + 2; + while (l->type) + { + switch (l->type) + { + case LLAYOUT_WIDTH: + w->width = WindowInfo.Width - w->x - Display_ScaleX(l->offset); + w->width = max(1, w->width); + break; + case LLAYOUT_HEIGHT: + w->height = WindowInfo.Height - w->y - Display_ScaleY(l->offset); + w->height = max(1, w->height); + break; + } + l++; + } +} + +static void LBackend_GetLayoutArgs(void* widget, jvalue* args) { + struct LWidget* w = (struct LWidget*)widget; + const struct LLayout* l = w->layouts; + + args[0].i = l[0].type & 0xFF; + args[1].i = Display_ScaleX(l[0].offset); + args[2].i = l[1].type & 0xFF; + args[3].i = Display_ScaleY(l[1].offset); +} + +void LBackend_LayoutWidget(struct LWidget* w) { + const struct LLayout* l = w->layouts; + // TODO remove this? once Table is done + + /* e.g. Table widget needs adjusts width/height based on window */ + if (l[1].type & LLAYOUT_EXTRA) + LBackend_LayoutDimensions(w); +} + +void LBackend_MarkDirty(void* widget) { } +void LBackend_InitFramebuffer(void) { } +void LBackend_FreeFramebuffer(void) { } + +static void JNICALL java_drawBackground(JNIEnv* env, jobject o, jobject bmp) { + Platform_LogConst("---$$$--"); + AndroidBitmapInfo info; + void* addr = NULL; + + AndroidBitmap_getInfo(env, bmp, &info); + AndroidBitmap_lockPixels(env, bmp, &addr); + + // TODO refactor this + struct Context2D ctx; + struct Bitmap pixels; + pixels.scan0 = addr; + pixels.width = info.width; + pixels.height = info.height; + Context2D_Wrap(&ctx, &pixels); + + struct LScreen* s = Launcher_Active; + if (s) s->DrawBackground(s, &ctx); + + AndroidBitmap_unlockPixels(env, bmp); +} + +void LBackend_Redraw(void) { + JNIEnv* env; + JavaGetCurrentEnv(env); + JavaCallVoid(env, "redrawBackground", "()V", NULL); +} + +static void LBackend_ButtonUpdateBackground(struct LButton* btn); +void LBackend_ThemeChanged(void) { + struct LScreen* s = Launcher_Active; + LBackend_Redraw(); + + for (int i = 0; i < s->numWidgets; i++) + { + struct LWidget* w = s->widgets[i]; + if (w->type != LWIDGET_BUTTON) continue; + LBackend_ButtonUpdateBackground((struct LButton*)w); + } +} + +void LBackend_Tick(void) { } + +static struct LWidget* FindWidgetForView(int id) { + struct LScreen* s = Launcher_Active; + for (int i = 0; i < s->numWidgets; i++) + { + void* meta = s->widgets[i]->meta; + if (meta != id) continue; + + return s->widgets[i]; + } + return NULL; +} + +static int ToAndroidColor(BitmapCol color) { + int R = BitmapCol_R(color); + int G = BitmapCol_G(color); + int B = BitmapCol_B(color); + int A = BitmapCol_A(color); + return (A << 24) | (R << 16) | (G << 8) | B; +} + + +static jstring JNICALL java_nextTextPart(JNIEnv* env, jobject o, jstring total, jintArray state) { + char buffer[NATIVE_STR_LEN]; + cc_string text = JavaGetString(env, total, buffer); + + jint* state_ = (*env)->GetIntArrayElements(env, state, NULL); + text.buffer += state_[0]; + text.length -= state_[0]; + + cc_string left = text, part; + char colorCode = 'f'; + + Drawer2D_UNSAFE_NextPart(&text, &part, &colorCode); + BitmapCol color = Drawer2D_GetColor(colorCode); + + state_[0] += left.length - text.length; + state_[1] = ToAndroidColor(color); + + (*env)->ReleaseIntArrayElements(env, state, state_, 0); + return JavaMakeString(env, &part); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Event handling------------------------------------------------------* +*#########################################################################################################################*/ +static void RequestRedraw(void* obj) { LBackend_Redraw(); } + +static void OnPointerUp(void* obj, int idx) { + Launcher_Active->MouseUp(Launcher_Active, idx); +} + +static void OnWindowCreated(void* obj) { + // e.g. after pause and resume + // TODO should pause/resume not trigger launcher screen recreation? + if (Launcher_Active) Launcher_SetScreen(Launcher_Active); +} + +static void HookEvents(void) { + Event_Register_(&WindowEvents.RedrawNeeded, NULL, RequestRedraw); + Event_Register_(&PointerEvents.Up, NULL, OnPointerUp); + Event_Register_(&WindowEvents.Created, NULL, OnWindowCreated); +} + + +/*########################################################################################################################* +*------------------------------------------------------ButtonWidget-------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_ButtonInit(struct LButton* w, int width, int height) { + w->_textWidth = Display_ScaleX(width); + w->_textHeight = Display_ScaleY(height); +} + +static void LBackend_ButtonShow(struct LButton* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[6]; + + LBackend_GetLayoutArgs(w, args); + args[4].i = w->_textWidth; + args[5].i = w->_textHeight; + + jmethodID method = JavaGetIMethod(env, "buttonAdd", "(IIIIII)I"); + w->meta = (void*)JavaICall_Int(env, method, args); +} + +void LBackend_ButtonUpdate(struct LButton* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[2]; + if (!w->meta) return; + + args[0].i = (int)w->meta; + args[1].l = JavaMakeString(env, &w->text); + + // TODO share logic with LabelUpdate/ButtonUpdate + jmethodID method = JavaGetIMethod(env, "buttonUpdate", "(ILjava/lang/String;)V"); + JavaICall_Void(env, method, args); + (*env)->DeleteLocalRef(env, args[1].l); +} +void LBackend_ButtonDraw(struct LButton* w) { } + +static void JNICALL java_makeButtonActive(JNIEnv* env, jobject o, jobject bmp) { + Platform_LogConst("---&&&--"); + AndroidBitmapInfo info; + void* addr = NULL; + + // TODO share code with drawBackground + AndroidBitmap_getInfo(env, bmp, &info); + AndroidBitmap_lockPixels(env, bmp, &addr); + + // TODO refactor this + struct Context2D ctx; + struct Bitmap pixels; + pixels.scan0 = addr; + pixels.width = info.width; + pixels.height = info.height; + Context2D_Wrap(&ctx, &pixels); + + LButton_DrawBackground(&ctx, 0, 0, info.width, info.height, true); + AndroidBitmap_unlockPixels(env, bmp); +} + +static void JNICALL java_makeButtonDefault(JNIEnv* env, jobject o, jobject bmp) { + Platform_LogConst("---####--"); + AndroidBitmapInfo info; + void* addr = NULL; + + // TODO share code with drawBackground + AndroidBitmap_getInfo(env, bmp, &info); + AndroidBitmap_lockPixels(env, bmp, &addr); + + // TODO refactor this + struct Context2D ctx; + struct Bitmap pixels; + pixels.scan0 = addr; + pixels.width = info.width; + pixels.height = info.height; + Context2D_Wrap(&ctx, &pixels); + + LButton_DrawBackground(&ctx, 0, 0, info.width, info.height, false); + AndroidBitmap_unlockPixels(env, bmp); +} + +static void LBackend_ButtonUpdateBackground(struct LButton* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[1]; + if (!w->meta) return; + + args[0].i = (int)w->meta; + jmethodID method = JavaGetIMethod(env, "buttonUpdateBackground", "(I)V"); + JavaICall_Void(env, method, args); +} + + +/*########################################################################################################################* +*-----------------------------------------------------CheckboxWidget------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_CheckboxInit(struct LCheckbox* w) { } + +static void LBackend_CheckboxShow(struct LCheckbox* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[6]; + + LBackend_GetLayoutArgs(w, args); + args[4].l = JavaMakeString(env, &w->text); + args[5].z = w->value; + + jmethodID method = JavaGetIMethod(env, "checkboxAdd", "(IIIILjava/lang/String;Z)I"); + w->meta = (void*)JavaICall_Int(env, method, args); + (*env)->DeleteLocalRef(env, args[4].l); +} +void LBackend_CheckboxDraw(struct LCheckbox* w) { } + +void LBackend_CheckboxUpdate(struct LCheckbox* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[2]; + if (!w->meta) return; + + args[0].i = (int)w->meta; + args[1].z = w->value; + + // TODO share logic with LabelUpdate/ButtonUpdate + jmethodID method = JavaGetIMethod(env, "checkboxUpdate", "(IZ)V"); + JavaICall_Void(env, method, args); +} + + +/*########################################################################################################################* +*------------------------------------------------------InputWidget--------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_InputInit(struct LInput* w, int width) { + w->_textHeight = Display_ScaleX(width); +} + +static void LBackend_InputShow(struct LInput* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[8]; + + LBackend_GetLayoutArgs(w, args); + args[4].i = w->_textHeight; + args[5].i = Display_ScaleY(LINPUT_HEIGHT); + args[6].i = w->inputType; + args[7].l = JavaMakeConst(env, w->hintText); + + jmethodID method = JavaGetIMethod(env, "inputAdd", "(IIIIIIILjava/lang/String;)I"); + w->meta = (void*)JavaICall_Int(env, method, args); + (*env)->DeleteLocalRef(env, args[7].l); +} + +void LBackend_InputUpdate(struct LInput* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[2]; + if (!w->meta) return; + + args[0].i = (int)w->meta; + args[1].l = JavaMakeString(env, &w->text); + + // TODO share logic with LabelUpdate/ButtonUpdate + jmethodID method = JavaGetIMethod(env, "inputUpdate", "(ILjava/lang/String;)V"); + JavaICall_Void(env, method, args); + (*env)->DeleteLocalRef(env, args[1].l); +} + +void LBackend_InputTick(struct LInput* w) { } +void LBackend_InputSelect(struct LInput* w, int idx, cc_bool wasSelected) { } +void LBackend_InputUnselect(struct LInput* w) { } +void LBackend_InputDraw(struct LInput* w) { } + + +/*########################################################################################################################* +*------------------------------------------------------LabelWidget--------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_LabelInit(struct LLabel* w) { } + +static void LBackend_LabelShow(struct LLabel* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[4]; + LBackend_GetLayoutArgs(w, args); + + jmethodID method = JavaGetIMethod(env, "labelAdd", "(IIII)I"); + w->meta = (void*)JavaICall_Int(env, method, args); +} + +void LBackend_LabelUpdate(struct LLabel* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[2]; + if (!w->meta) return; + + args[0].i = (int)w->meta; + args[1].l = JavaMakeString(env, &w->text); + + // TODO share logic with LabelUpdate/ButtonUpdate + jmethodID method = JavaGetIMethod(env, "labelUpdate", "(ILjava/lang/String;)V"); + JavaICall_Void(env, method, args); + (*env)->DeleteLocalRef(env, args[1].l); +} +void LBackend_LabelDraw(struct LLabel* w) { } + + +/*########################################################################################################################* +*-------------------------------------------------------LineWidget--------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_LineInit(struct LLine* w, int width) { + w->_width = Display_ScaleX(width); +} + +static void LBackend_LineShow(struct LLine* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[7]; + + LBackend_GetLayoutArgs(w, args); + args[4].i = w->_width; + args[5].i = Display_ScaleY(LLINE_HEIGHT); + args[6].i = ToAndroidColor(LLine_GetColor()); + + jmethodID method = JavaGetIMethod(env, "lineAdd", "(IIIIIII)I"); + w->meta = (void*)JavaICall_Int(env, method, args); +} +void LBackend_LineDraw(struct LLine* w) { } + + +/*########################################################################################################################* +*------------------------------------------------------SliderWidget-------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_SliderInit(struct LSlider* w, int width, int height) { + w->_width = Display_ScaleX(width); + w->_height = Display_ScaleY(height); +} + +static void LBackend_SliderShow(struct LSlider* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[7]; + + LBackend_GetLayoutArgs(w, args); + args[4].i = w->_width; + args[5].i = w->_height; + args[6].i = ToAndroidColor(w->color); + + jmethodID method = JavaGetIMethod(env, "sliderAdd", "(IIIIIII)I"); + w->meta = (void*)JavaICall_Int(env, method, args); +} + +void LBackend_SliderUpdate(struct LSlider* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[2]; + if (!w->meta) return; + + args[0].i = (int)w->meta; + args[1].i = w->value; + + jmethodID method = JavaGetIMethod(env, "sliderUpdate", "(II)V"); + JavaICall_Void(env, method, args); +} +void LBackend_SliderDraw(struct LSlider* w) {} + + +/*########################################################################################################################* +*-------------------------------------------------------TableWidget-------------------------------------------------------* +*#########################################################################################################################*/ +void LBackend_TableInit(struct LTable* w) { + +} + +static void LBackend_TableShow(struct LTable* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[5]; + + LBackend_GetLayoutArgs(w, args); + args[4].i = ToAndroidColor(LTable_RowColor(1, false, false)); + + jmethodID method = JavaGetIMethod(env, "tableAdd", "(IIIII)I"); + w->meta = (void*)JavaICall_Int(env, method, args); + LBackend_TableUpdate(w); +} + +static jstring GetTableDetails(JNIEnv* env, struct ServerInfo* server) { + char buffer[NATIVE_STR_LEN]; + cc_string text = String_FromArray(buffer); + + String_Format2(&text, "%i/%i players, up for ", &server->players, &server->maxPlayers); + LTable_FormatUptime(&text, server->uptime); + if (server->software.length) String_Format1(&text, " | %s", &server->software); + + return JavaMakeString(env, &text); +} + +void LBackend_TableUpdate(struct LTable* w) { + JNIEnv* env; JavaGetCurrentEnv(env); + jvalue args[3]; + jmethodID method; + + method = JavaGetIMethod(env, "tableStartUpdate", "()V"); + JavaICall_Void(env, method, args); + method = JavaGetIMethod(env, "tableAddEntry", "(Ljava/lang/String;Ljava/lang/String;Z)V"); + + for (int i = 0; i < w->rowsCount; i++) + { + struct ServerInfo* info = LTable_Get(i); + args[0].l = JavaMakeString(env, &info->name); + args[1].l = GetTableDetails(env, info); + args[2].z = info->featured; + JavaICall_Void(env, method, args); + + (*env)->DeleteLocalRef(env, args[0].l); + (*env)->DeleteLocalRef(env, args[1].l); + } + + method = JavaGetIMethod(env, "tableFinishUpdate", "(I)V"); + args[0].i = (int)w->meta; + JavaICall_Void(env, method, args); +} + +void LBackend_TableReposition(struct LTable* w) { + +} + +void LBackend_TableFlagAdded(struct LTable* w) { +} + +void LBackend_TableDraw(struct LTable* w) { } +void LBackend_TableMouseDown(struct LTable* w, int idx) { } +void LBackend_TableMouseMove(struct LTable* w, int idx) { } +void LBackend_TableMouseUp(struct LTable* w, int idx) { } + +static jint JNICALL java_tableGetColor(JNIEnv* env, jobject o, jint row, jboolean selected, jboolean featured) { + return ToAndroidColor(LTable_RowColor(row, selected, featured)); +} + + +/*########################################################################################################################* +*--------------------------------------------------------UIBackend--------------------------------------------------------* +*#########################################################################################################################*/ +#define UI_EVENT_CLICKED 1 +#define UI_EVENT_CHANGED 2 + +static void JNICALL java_UIClicked(JNIEnv* env, jobject o, jint id) { + struct LWidget* w = FindWidgetForView(id); + if (!w) return; + + if (w->OnClick) w->OnClick(w); +} + +static void JNICALL java_UIChanged(JNIEnv* env, jobject o, jint id, jint val) { + struct LCheckbox* cb = (struct LCheckbox*)FindWidgetForView(id); + if (!cb) return; + + cb->value = val; + if (cb->ValueChanged) cb->ValueChanged(cb); +} + +static void JNICALL java_UIString(JNIEnv* env, jobject o, jint id, jstring str) { + struct LInput* ipt = (struct LInput*)FindWidgetForView(id); + if (!ipt) return; + + char buffer[NATIVE_STR_LEN]; + cc_string text = JavaGetString(env, str, buffer); + String_Copy(&ipt->text, &text); + if (ipt->TextChanged) ipt->TextChanged(ipt); +} + +static void ShowWidget(struct LWidget* w) { + switch (w->type) { + case LWIDGET_BUTTON: + LBackend_ButtonShow((struct LButton*)w); + LBackend_ButtonUpdate((struct LButton*)w); + break; + case LWIDGET_CHECKBOX: + LBackend_CheckboxShow((struct LCheckbox*)w); + break; + case LWIDGET_INPUT: + LBackend_InputShow((struct LInput*)w); + LBackend_InputUpdate((struct LInput*)w); + break; + case LWIDGET_LABEL: + LBackend_LabelShow((struct LLabel*)w); + LBackend_LabelUpdate((struct LLabel*)w); + break; + case LWIDGET_LINE: + LBackend_LineShow((struct LLine*)w); + break; + case LWIDGET_SLIDER: + LBackend_SliderShow((struct LSlider*)w); + break; + case LWIDGET_TABLE: + LBackend_TableShow((struct LTable*)w); + break; + } +} + +void LBackend_SetScreen(struct LScreen* s) { + for (int i = 0; i < s->numWidgets; i++) + { + ShowWidget(s->widgets[i]); + } +} + +void LBackend_CloseScreen(struct LScreen* s) { + // stop referencing widgets + for (int i = 0; i < s->numWidgets; i++) + { + s->widgets[i]->meta = NULL; + } + + JNIEnv* env; JavaGetCurrentEnv(env); + jmethodID method = JavaGetIMethod(env, "clearWidgetsAsync", "()V"); + JavaICall_Void(env, method, NULL); +} + +static const JNINativeMethod methods[] = { + { "nextTextPart", "(Ljava/lang/String;[I)Ljava/lang/String;", java_nextTextPart }, + { "drawBackground", "(Landroid/graphics/Bitmap;)V", java_drawBackground }, + { "makeButtonActive", "(Landroid/graphics/Bitmap;)V", java_makeButtonActive }, + { "makeButtonDefault", "(Landroid/graphics/Bitmap;)V", java_makeButtonDefault }, + { "processOnUIClicked", "(I)V", java_UIClicked }, + { "processOnUIChanged", "(II)V", java_UIChanged }, + { "processOnUIString", "(ILjava/lang/String;)V", java_UIString }, + { "tableGetColor", "(IZZ)I", java_tableGetColor }, +}; + +static void LBackend_InitHooks(void) { + JNIEnv* env; + JavaGetCurrentEnv(env); + JavaRegisterNatives(env, methods); +} +#endif \ No newline at end of file diff --git a/src/LScreens.c b/src/LScreens.c new file mode 100644 index 0000000..3362389 --- /dev/null +++ b/src/LScreens.c @@ -0,0 +1,1856 @@ +#include "LScreens.h" +#ifndef CC_BUILD_WEB +#include "String.h" +#include "LWidgets.h" +#include "LWeb.h" +#include "Launcher.h" +#include "Gui.h" +#include "Drawer2D.h" +#include "ExtMath.h" +#include "Platform.h" +#include "Stream.h" +#include "Funcs.h" +#include "Resources.h" +#include "Logger.h" +#include "Window.h" +#include "Input.h" +#include "Options.h" +#include "Utils.h" +#include "LBackend.h" +#include "Http.h" +#include "Game.h" + +#define LAYOUTS static const struct LLayout +#define IsEnterButton(btn) (btn == CCKEY_ENTER || btn == CCPAD_START || btn == CCPAD_A || btn == CCKEY_KP_ENTER) +#define IsBackButton(btn) (btn == CCKEY_ESCAPE || btn == CCPAD_SELECT || btn == CCPAD_B) + +/*########################################################################################################################* +*---------------------------------------------------------Screen base-----------------------------------------------------* +*#########################################################################################################################*/ +static void LScreen_NullFunc(struct LScreen* s) { } +CC_NOINLINE static int LScreen_IndexOf(struct LScreen* s, void* w) { + int i; + for (i = 0; i < s->numWidgets; i++) { + if (s->widgets[i] == w) return i; + } + return -1; +} + +static void LScreen_DoLayout(struct LScreen* s) { + int i; + for (i = 0; i < s->numWidgets; i++) + { + LBackend_LayoutWidget(s->widgets[i]); + } +} + +static void LScreen_Tick(struct LScreen* s) { + struct LWidget* w = s->selectedWidget; + if (w && w->VTABLE->Tick) w->VTABLE->Tick(w); +} + +void LScreen_SelectWidget(struct LScreen* s, int idx, struct LWidget* w, cc_bool was) { + if (!w) return; + w->selected = true; + s->selectedWidget = w; + if (w->VTABLE->OnSelect) w->VTABLE->OnSelect(w, idx, was); +} + +void LScreen_UnselectWidget(struct LScreen* s, int idx, struct LWidget* w) { + if (!w) return; + w->selected = false; + s->selectedWidget = NULL; + if (w->VTABLE->OnUnselect) w->VTABLE->OnUnselect(w, idx); +} + +static void LScreen_CycleSelected(struct LScreen* s, int dir) { + struct LWidget* w; + int index = 0, i, j; + + if (s->selectedWidget) { + index = LScreen_IndexOf(s, s->selectedWidget) + dir; + } + + for (j = 0; j < s->numWidgets; j++) { + i = (index + j * dir) % s->numWidgets; + if (i < 0) i += s->numWidgets; + + w = s->widgets[i]; + if (!w->autoSelectable) continue; + + LScreen_UnselectWidget(s, 0, s->selectedWidget); + LScreen_SelectWidget(s, 0, w, false); + return; + } +} + +static void LScreen_KeyDown(struct LScreen* s, int key, cc_bool was) { + if (IsEnterButton(key)) { + /* Shouldn't multi click when holding down Enter */ + if (was) return; + + if (s->selectedWidget && s->selectedWidget->OnClick) { + s->selectedWidget->OnClick(s->selectedWidget); + } else if (s->hoveredWidget && s->hoveredWidget->OnClick) { + s->hoveredWidget->OnClick(s->hoveredWidget); + } else if (s->onEnterWidget) { + s->onEnterWidget->OnClick(s->onEnterWidget); + } + return; + } + + /* Active widget takes input priority over default behaviour */ + if (s->selectedWidget && s->selectedWidget->VTABLE->KeyDown) { + if (s->selectedWidget->VTABLE->KeyDown(s->selectedWidget, key, was)) return; + } + + if (key == CCKEY_TAB || key == CCPAD_X) { + LScreen_CycleSelected(s, Input_IsShiftPressed() ? -1 : 1); + } else if (Input_IsUpButton(key)) { + LScreen_CycleSelected(s, -1); + } else if (Input_IsDownButton(key)) { + LScreen_CycleSelected(s, 1); + } else if (IsBackButton(key) && s->onEscapeWidget) { + s->onEscapeWidget->OnClick(s->onEscapeWidget); + } +} + +static void LScreen_MouseUp(struct LScreen* s, int idx) { } +static void LScreen_MouseWheel(struct LScreen* s, float delta) { } + +static void LScreen_DrawBackground(struct LScreen* s, struct Context2D* ctx) { + if (!s->title) { + Launcher_DrawBackground(ctx, 0, 0, ctx->width, ctx->height); + return; + } + Launcher_DrawBackgroundAll(ctx); + LBackend_DrawTitle(ctx, s->title); +} + +CC_NOINLINE static void LScreen_Reset(struct LScreen* s) { + int i; + + s->Activated = LScreen_NullFunc; + s->LoadState = LScreen_NullFunc; + s->Deactivated = LScreen_NullFunc; + s->Layout = LScreen_DoLayout; + s->Tick = LScreen_Tick; + s->KeyDown = LScreen_KeyDown; + s->MouseUp = LScreen_MouseUp; + s->MouseWheel = LScreen_MouseWheel; + s->DrawBackground = LScreen_DrawBackground; + s->ResetArea = Launcher_DrawBackground; + + /* reset all widgets mouse state */ + for (i = 0; i < s->numWidgets; i++) + { + s->widgets[i]->hovered = false; + s->widgets[i]->selected = false; + } + + s->numWidgets = 0; + s->hoveredWidget = NULL; + s->selectedWidget = NULL; +} + + +void LScreen_AddWidget(void* screen, void* widget) { + struct LScreen* s = (struct LScreen*)screen; + struct LWidget* w = (struct LWidget*)widget; + + if (s->numWidgets >= s->maxWidgets) + Logger_Abort("Can't add anymore widgets to this LScreen"); + s->widgets[s->numWidgets++] = w; +} + + +static void SwitchToChooseMode(void* w) { ChooseModeScreen_SetActive(false); } +static void SwitchToColours(void* w) { ColoursScreen_SetActive(); } +static void SwitchToDirectConnect(void* w) { DirectConnectScreen_SetActive(); } +static void SwitchToMain(void* w) { MainScreen_SetActive(); } +static void SwitchToSettings(void* w) { SettingsScreen_SetActive(); } +static void SwitchToThemes(void* w) { ThemesScreen_SetActive(); } +static void SwitchToUpdates(void* w) { UpdatesScreen_SetActive(); } + + +/*########################################################################################################################* +*-------------------------------------------------------ChooseModeScreen--------------------------------------------------* +*#########################################################################################################################*/ +static struct ChooseModeScreen { + LScreen_Layout + struct LLine seps[2]; + struct LButton btnEnhanced, btnClassicHax, btnClassic, btnBack; + struct LLabel lblHelp, lblEnhanced[2], lblClassicHax[2], lblClassic[2]; + cc_bool firstTime; +} ChooseModeScreen; + +#define CHOOSEMODE_SCREEN_MAX_WIDGETS 12 +static struct LWidget* chooseMode_widgets[CHOOSEMODE_SCREEN_MAX_WIDGETS]; + +LAYOUTS mode_seps0[] = { { ANCHOR_CENTRE, -5 }, { ANCHOR_CENTRE, -85 } }; +LAYOUTS mode_seps1[] = { { ANCHOR_CENTRE, -5 }, { ANCHOR_CENTRE, -15 } }; + +LAYOUTS mode_btnEnhanced[] = { { ANCHOR_CENTRE_MIN, -250 }, { ANCHOR_CENTRE, -120 } }; +LAYOUTS mode_lblEnhanced0[] = { { ANCHOR_CENTRE_MIN, -85 }, { ANCHOR_CENTRE, -120 - 12 } }; +LAYOUTS mode_lblEnhanced1[] = { { ANCHOR_CENTRE_MIN, -85 }, { ANCHOR_CENTRE, -120 + 12 } }; +LAYOUTS mode_btnClassicHax[] = { { ANCHOR_CENTRE_MIN, -250 }, { ANCHOR_CENTRE, -50 } }; +LAYOUTS mode_lblClassicHax0[] = { { ANCHOR_CENTRE_MIN, -85 }, { ANCHOR_CENTRE, -50 - 12 } }; +LAYOUTS mode_lblClassicHax1[] = { { ANCHOR_CENTRE_MIN, -85 }, { ANCHOR_CENTRE, -50 + 12 } }; +LAYOUTS mode_btnClassic[] = { { ANCHOR_CENTRE_MIN, -250 }, { ANCHOR_CENTRE, 20 } }; +LAYOUTS mode_lblClassic0[] = { { ANCHOR_CENTRE_MIN, -85 }, { ANCHOR_CENTRE, 20 - 12 } }; +LAYOUTS mode_lblClassic1[] = { { ANCHOR_CENTRE_MIN, -85 }, { ANCHOR_CENTRE, 20 + 12 } }; + +LAYOUTS mode_lblHelp[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 160 } }; +LAYOUTS mode_btnBack[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 170 } }; + + +CC_NOINLINE static void ChooseMode_Click(cc_bool classic, cc_bool classicHacks) { + Options_SetBool(OPT_CLASSIC_MODE, classic); + if (classic) Options_SetBool(OPT_CLASSIC_HACKS, classicHacks); + + Options_SetBool(OPT_CUSTOM_BLOCKS, !classic); + Options_SetBool(OPT_CPE, !classic); + Options_SetBool(OPT_SERVER_TEXTURES, !classic); + Options_SetBool(OPT_CLASSIC_TABLIST, classic); + Options_SetBool(OPT_CLASSIC_OPTIONS, classic); + + Options_SaveIfChanged(); + Launcher_LoadTheme(); + LBackend_UpdateTitleFont(); + MainScreen_SetActive(); +} + +static void UseModeEnhanced(void* w) { ChooseMode_Click(false, false); } +static void UseModeClassicHax(void* w) { ChooseMode_Click(true, true); } +static void UseModeClassic(void* w) { ChooseMode_Click(true, false); } + +static void ChooseModeScreen_Activated(struct LScreen* s_) { + struct ChooseModeScreen* s = (struct ChooseModeScreen*)s_; + LLine_Add(s, &s->seps[0], 490, mode_seps0); + LLine_Add(s, &s->seps[1], 490, mode_seps1); + + LButton_Add(s, &s->btnEnhanced, 145, 35, "Enhanced", + UseModeEnhanced, mode_btnEnhanced); + LLabel_Add(s, &s->lblEnhanced[0], "&eEnables custom blocks, changing env", mode_lblEnhanced0); + LLabel_Add(s, &s->lblEnhanced[1], "&esettings, longer messages, and more", mode_lblEnhanced1); + + LButton_Add(s, &s->btnClassicHax, 145, 35, "Classic +hax", + UseModeClassicHax, mode_btnClassicHax); + LLabel_Add(s, &s->lblClassicHax[0], "&eSame as Classic mode, except that", mode_lblClassicHax0); + LLabel_Add(s, &s->lblClassicHax[1], "&ehacks (noclip/fly/speed) are enabled", mode_lblClassicHax1); + + LButton_Add(s, &s->btnClassic, 145, 35, "Classic", + UseModeClassic, mode_btnClassic); + LLabel_Add(s, &s->lblClassic[0], "&eOnly uses blocks and features from", mode_lblClassic0); + LLabel_Add(s, &s->lblClassic[1], "ðe original minecraft classic", mode_lblClassic1); + + if (s->firstTime) { + LLabel_Add(s, &s->lblHelp, "&eClick &fEnhanced &eif you're not sure which mode to choose.", mode_lblHelp); + } else { + LButton_Add(s, &s->btnBack, 80, 35, "Back", + SwitchToSettings, mode_btnBack); + } +} + +void ChooseModeScreen_SetActive(cc_bool firstTime) { + struct ChooseModeScreen* s = &ChooseModeScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = chooseMode_widgets; + s->maxWidgets = Array_Elems(chooseMode_widgets); + s->firstTime = firstTime; + + s->Activated = ChooseModeScreen_Activated; + s->title = "Choose mode"; + s->onEnterWidget = (struct LWidget*)&s->btnEnhanced; + s->onEscapeWidget = firstTime ? NULL : (struct LWidget*)&s->btnBack; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*---------------------------------------------------------ColoursScreen---------------------------------------------------* +*#########################################################################################################################*/ +#define COLOURS_NUM_ROWS 5 /* Background, border, etc */ +#define COLOURS_NUM_COLS 3 /* R, G, B widgets */ +#define COLOURS_NUM_ENTRIES (COLOURS_NUM_ROWS * COLOURS_NUM_COLS) + +static struct ColoursScreen { + LScreen_Layout + struct LButton btnBack; + struct LLabel lblNames[COLOURS_NUM_ROWS]; + struct LLabel lblRGB[COLOURS_NUM_COLS]; + struct LInput iptColours[COLOURS_NUM_ENTRIES]; + struct LCheckbox cbClassic; +} ColoursScreen; + +#define COLOURSSCREEN_MAX_WIDGETS 25 +static struct LWidget* colours_widgets[COLOURSSCREEN_MAX_WIDGETS]; + +#define IptColor_Layout(xx, yy) { { ANCHOR_CENTRE, xx }, { ANCHOR_CENTRE, yy } } +LAYOUTS clr_iptColours[COLOURS_NUM_ENTRIES][2] = { + IptColor_Layout(30, -100), IptColor_Layout(95, -100), IptColor_Layout(160, -100), + IptColor_Layout(30, -60), IptColor_Layout(95, -60), IptColor_Layout(160, -60), + IptColor_Layout(30, -20), IptColor_Layout(95, -20), IptColor_Layout(160, -20), + IptColor_Layout(30, 20), IptColor_Layout(95, 20), IptColor_Layout(160, 20), + IptColor_Layout(30, 60), IptColor_Layout(95, 60), IptColor_Layout(160, 60), +}; +LAYOUTS clr_lblNames0[] = { { ANCHOR_CENTRE_MAX, 10 }, { ANCHOR_CENTRE, -100 } }; +LAYOUTS clr_lblNames1[] = { { ANCHOR_CENTRE_MAX, 10 }, { ANCHOR_CENTRE, -60 } }; +LAYOUTS clr_lblNames2[] = { { ANCHOR_CENTRE_MAX, 10 }, { ANCHOR_CENTRE, -20 } }; +LAYOUTS clr_lblNames3[] = { { ANCHOR_CENTRE_MAX, 10 }, { ANCHOR_CENTRE, 20 } }; +LAYOUTS clr_lblNames4[] = { { ANCHOR_CENTRE_MAX, 10 }, { ANCHOR_CENTRE, 60 } }; + +LAYOUTS clr_lblRGB0[] = { { ANCHOR_CENTRE, 30 }, { ANCHOR_CENTRE, -130 } }; +LAYOUTS clr_lblRGB1[] = { { ANCHOR_CENTRE, 95 }, { ANCHOR_CENTRE, -130 } }; +LAYOUTS clr_lblRGB2[] = { { ANCHOR_CENTRE, 160 }, { ANCHOR_CENTRE, -130 } }; +LAYOUTS clr_cbClassic[] = { { ANCHOR_CENTRE, -16 }, { ANCHOR_CENTRE, 130 } }; +LAYOUTS clr_btnBack[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 170 } }; + + +CC_NOINLINE static void ColoursScreen_Set(struct LInput* w, cc_uint8 value) { + cc_string tmp; char tmpBuffer[STRING_SIZE]; + String_InitArray(tmp, tmpBuffer); + + String_AppendInt(&tmp, value); + LInput_SetText(w, &tmp); +} + +CC_NOINLINE static void ColoursScreen_Update(struct ColoursScreen* s, int i, BitmapCol color) { + ColoursScreen_Set(&s->iptColours[i + 0], BitmapCol_R(color)); + ColoursScreen_Set(&s->iptColours[i + 1], BitmapCol_G(color)); + ColoursScreen_Set(&s->iptColours[i + 2], BitmapCol_B(color)); +} + +CC_NOINLINE static void ColoursScreen_UpdateAll(struct ColoursScreen* s) { + ColoursScreen_Update(s, 0, Launcher_Theme.BackgroundColor); + ColoursScreen_Update(s, 3, Launcher_Theme.ButtonBorderColor); + ColoursScreen_Update(s, 6, Launcher_Theme.ButtonHighlightColor); + ColoursScreen_Update(s, 9, Launcher_Theme.ButtonForeColor); + ColoursScreen_Update(s, 12, Launcher_Theme.ButtonForeActiveColor); +} + +static void ColoursScreen_TextChanged(struct LInput* w) { + struct ColoursScreen* s = &ColoursScreen; + int index = LScreen_IndexOf((struct LScreen*)s, w); + BitmapCol* color; + cc_uint8 r, g, b; + + if (index < 3) color = &Launcher_Theme.BackgroundColor; + else if (index < 6) color = &Launcher_Theme.ButtonBorderColor; + else if (index < 9) color = &Launcher_Theme.ButtonHighlightColor; + else if (index < 12) color = &Launcher_Theme.ButtonForeColor; + else color = &Launcher_Theme.ButtonForeActiveColor; + + /* if index of G input, changes to index of R input */ + index = (index / 3) * 3; + if (!Convert_ParseUInt8(&s->iptColours[index + 0].text, &r)) return; + if (!Convert_ParseUInt8(&s->iptColours[index + 1].text, &g)) return; + if (!Convert_ParseUInt8(&s->iptColours[index + 2].text, &b)) return; + + *color = BitmapColor_RGB(r, g, b); + Launcher_SaveTheme(); + LBackend_ThemeChanged(); +} + +static void ColoursScreen_AdjustSelected(struct LScreen* s, int delta) { + struct LInput* w; + int index, newVal; + cc_uint8 value; + + if (!s->selectedWidget) return; + index = LScreen_IndexOf(s, s->selectedWidget); + if (index >= 15) return; + + w = (struct LInput*)s->selectedWidget; + if (!Convert_ParseUInt8(&w->text, &value)) return; + newVal = value + delta; + + Math_Clamp(newVal, 0, 255); + ColoursScreen_Set(w, newVal); + ColoursScreen_TextChanged(w); +} + +static void ColoursScreen_KeyDown(struct LScreen* s, int key, cc_bool was) { + int delta = Input_CalcDelta(key, 1, 10); + if (key == CCWHEEL_UP) delta = +1; + if (key == CCWHEEL_DOWN) delta = -1; + + if (delta) { + ColoursScreen_AdjustSelected(s, delta); + } else { + LScreen_KeyDown(s, key, was); + } +} + +static void ColoursScreen_ToggleBG(struct LCheckbox* w) { + Launcher_Theme.ClassicBackground = w->value; + Launcher_SaveTheme(); + LBackend_ThemeChanged(); +} + +static void ColoursScreen_AddWidgets(struct ColoursScreen* s) { + int i; + for (i = 0; i < COLOURS_NUM_ENTRIES; i++) + { + s->iptColours[i].inputType = KEYBOARD_TYPE_INTEGER; + s->iptColours[i].TextChanged = ColoursScreen_TextChanged; + LInput_Add(s, &s->iptColours[i], 55, NULL, clr_iptColours[i]); + } + + LLabel_Add(s, &s->lblNames[0], "Background", clr_lblNames0); + LLabel_Add(s, &s->lblNames[1], "Button border", clr_lblNames1); + LLabel_Add(s, &s->lblNames[2], "Button highlight", clr_lblNames2); + LLabel_Add(s, &s->lblNames[3], "Button", clr_lblNames3); + LLabel_Add(s, &s->lblNames[4], "Active button", clr_lblNames4); + + LLabel_Add(s, &s->lblRGB[0], "Red", clr_lblRGB0); + LLabel_Add(s, &s->lblRGB[1], "Green", clr_lblRGB1); + LLabel_Add(s, &s->lblRGB[2], "Blue", clr_lblRGB2); + LButton_Add(s, &s->btnBack, 80, 35, "Back", + SwitchToThemes, clr_btnBack); + + LCheckbox_Add(s, &s->cbClassic, "Classic style", + ColoursScreen_ToggleBG, clr_cbClassic); +} + +static void ColoursScreen_Activated(struct LScreen* s_) { + struct ColoursScreen* s = (struct ColoursScreen*)s_; + ColoursScreen_AddWidgets(s); + + LCheckbox_Set(&s->cbClassic, Launcher_Theme.ClassicBackground); + ColoursScreen_UpdateAll(s); +} + +void ColoursScreen_SetActive(void) { + struct ColoursScreen* s = &ColoursScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = colours_widgets; + s->maxWidgets = Array_Elems(colours_widgets); + + s->Activated = ColoursScreen_Activated; + s->KeyDown = ColoursScreen_KeyDown; + + s->title = "Custom theme"; + s->onEscapeWidget = (struct LWidget*)&s->btnBack; + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*------------------------------------------------------DirectConnectScreen------------------------------------------------* +*#########################################################################################################################*/ +static struct DirectConnectScreen { + LScreen_Layout + struct LButton btnConnect, btnBack; + struct LInput iptUsername, iptAddress, iptMppass; + struct LLabel lblStatus; +} DirectConnectScreen; + +#define DIRECTCONNECT_SCREEN_MAXWIDGETS 6 +static struct LWidget* directConnect_widgets[DIRECTCONNECT_SCREEN_MAXWIDGETS]; + +LAYOUTS dc_iptUsername[] = { { ANCHOR_CENTRE_MIN, -165 }, { ANCHOR_CENTRE, -120 } }; +LAYOUTS dc_iptAddress[] = { { ANCHOR_CENTRE_MIN, -165 }, { ANCHOR_CENTRE, -75 } }; +LAYOUTS dc_iptMppass[] = { { ANCHOR_CENTRE_MIN, -165 }, { ANCHOR_CENTRE, -30 } }; + +LAYOUTS dc_btnConnect[] = { { ANCHOR_CENTRE, -110 }, { ANCHOR_CENTRE, 20 } }; +LAYOUTS dc_btnBack[] = { { ANCHOR_CENTRE, 125 }, { ANCHOR_CENTRE, 20 } }; +LAYOUTS dc_lblStatus[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 70 } }; + + +static void DirectConnectScreen_UrlFilter(cc_string* str) { + static const cc_string prefix = String_FromConst("mc://"); + cc_string parts[6]; + if (!String_CaselessStarts(str, &prefix)) return; + /* mc://[ip:port]/[username]/[mppass] */ + if (String_UNSAFE_Split(str, '/', parts, 6) != 5) return; + + LInput_SetString(&DirectConnectScreen.iptAddress, &parts[2]); + LInput_SetString(&DirectConnectScreen.iptUsername, &parts[3]); + LInput_SetString(&DirectConnectScreen.iptMppass, &parts[4]); + str->length = 0; +} + +static void DirectConnectScreen_StartClient(void* w) { + static const cc_string defMppass = String_FromConst("(none)"); + static const cc_string defPort = String_FromConst("25565"); + const cc_string* user = &DirectConnectScreen.iptUsername.text; + const cc_string* addr = &DirectConnectScreen.iptAddress.text; + const cc_string* mppass = &DirectConnectScreen.iptMppass.text; + struct LLabel* status = &DirectConnectScreen.lblStatus; + + cc_string ip, port; + cc_uint16 raw_port; + cc_sockaddr addrs[SOCKET_MAX_ADDRS]; + int numAddrs; + + int index = String_LastIndexOf(addr, ':'); + if (index == 0 || index == addr->length - 1) { + LLabel_SetConst(status, "&cInvalid address"); return; + } + + /* support either "[IP]" or "[IP]:[PORT]" */ + if (index == -1) { + ip = *addr; + port = defPort; + } else { + ip = String_UNSAFE_Substring(addr, 0, index); + port = String_UNSAFE_SubstringAt(addr, index + 1); + } + + if (!user->length) { + LLabel_SetConst(status, "&cUsername required"); return; + } + if (Socket_ParseAddress(&ip, 0, addrs, &numAddrs)) { + LLabel_SetConst(status, "&cInvalid ip"); return; + } + if (!Convert_ParseUInt16(&port, &raw_port)) { + LLabel_SetConst(status, "&cInvalid port"); return; + } + if (!mppass->length) mppass = &defMppass; + + Options_PauseSaving(); + Options_Set("launcher-dc-username", user); + Options_Set("launcher-dc-ip", &ip); + Options_Set("launcher-dc-port", &port); + Options_SetSecure("launcher-dc-mppass", mppass); + Options_ResumeSaving(); + + LLabel_SetConst(status, ""); + Launcher_StartGame(user, mppass, &ip, &port, &String_Empty); +} + +static void DirectConnectScreen_Activated(struct LScreen* s_) { + struct DirectConnectScreen* s = (struct DirectConnectScreen*)s_; + + LInput_Add(s, &s->iptUsername, 330, "Username..", dc_iptUsername); + LInput_Add(s, &s->iptAddress, 330, "IP address:Port number..", dc_iptAddress); + LInput_Add(s, &s->iptMppass, 330, "Mppass..", dc_iptMppass); + + LButton_Add(s, &s->btnConnect, 110, 35, "Connect", + DirectConnectScreen_StartClient, dc_btnConnect); + LButton_Add(s, &s->btnBack, 80, 35, "Back", + SwitchToMain, dc_btnBack); + LLabel_Add(s, &s->lblStatus, "", dc_lblStatus); + + s->iptUsername.ClipboardFilter = DirectConnectScreen_UrlFilter; + s->iptAddress.ClipboardFilter = DirectConnectScreen_UrlFilter; + s->iptMppass.ClipboardFilter = DirectConnectScreen_UrlFilter; +} + +static void DirectConnectScreen_Load(struct LScreen* s_) { + struct DirectConnectScreen* s = (struct DirectConnectScreen*)s_; + cc_string addr; char addrBuffer[STRING_SIZE]; + cc_string mppass; char mppassBuffer[STRING_SIZE]; + cc_string user, ip, port; + Options_Reload(); + + Options_UNSAFE_Get("launcher-dc-username", &user); + Options_UNSAFE_Get("launcher-dc-ip", &ip); + Options_UNSAFE_Get("launcher-dc-port", &port); + + String_InitArray(mppass, mppassBuffer); + Options_GetSecure("launcher-dc-mppass", &mppass); + String_InitArray(addr, addrBuffer); + String_Format2(&addr, "%s:%s", &ip, &port); + + /* don't want just ':' for address */ + if (addr.length == 1) addr.length = 0; + + LInput_SetText(&s->iptUsername, &user); + LInput_SetText(&s->iptAddress, &addr); + LInput_SetText(&s->iptMppass, &mppass); +} + +void DirectConnectScreen_SetActive(void) { + struct DirectConnectScreen* s = &DirectConnectScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = directConnect_widgets; + s->maxWidgets = Array_Elems(directConnect_widgets); + + s->Activated = DirectConnectScreen_Activated; + s->LoadState = DirectConnectScreen_Load; + s->title = "Direct connect"; + s->onEnterWidget = (struct LWidget*)&s->btnConnect; + s->onEscapeWidget = (struct LWidget*)&s->btnBack; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*----------------------------------------------------------MFAScreen------------------------------------------------------* +*#########################################################################################################################*/ +static struct MFAScreen { + LScreen_Layout + struct LInput iptCode; + struct LButton btnSignIn, btnCancel; + struct LLabel lblTitle; +} MFAScreen; + +#define MFA_SCREEN_MAX_WIDGETS 4 +static struct LWidget* mfa_widgets[MFA_SCREEN_MAX_WIDGETS]; + +LAYOUTS mfa_lblTitle[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -115 } }; +LAYOUTS mfa_iptCode[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -75 } }; +LAYOUTS mfa_btnSignIn[] = { { ANCHOR_CENTRE, -90 }, { ANCHOR_CENTRE, -25 } }; +LAYOUTS mfa_btnCancel[] = { { ANCHOR_CENTRE, 90 }, { ANCHOR_CENTRE, -25 } }; + + +static void MainScreen_DoLogin(void); +static void MFAScreen_SignIn(void* w) { + MainScreen_SetActive(); + MainScreen_DoLogin(); +} +static void MFAScreen_Cancel(void* w) { + LInput_ClearText(&MFAScreen.iptCode); + MainScreen_SetActive(); +} + +static void MFAScreen_Activated(struct LScreen* s_) { + struct MFAScreen* s = (struct MFAScreen*)s_; + s->iptCode.inputType = KEYBOARD_TYPE_INTEGER; + + LLabel_Add(s, &s->lblTitle, "", mfa_lblTitle); + LInput_Add(s, &s->iptCode, 280, "Login code..", mfa_iptCode); + LButton_Add(s, &s->btnSignIn, 100, 35, "Sign in", + MFAScreen_SignIn, mfa_btnSignIn); + LButton_Add(s, &s->btnCancel, 100, 35, "Cancel", + MFAScreen_Cancel, mfa_btnCancel); + + LLabel_SetConst(&s->lblTitle, s->iptCode.text.length ? + "&cWrong code entered (Check emails)" : + "&cLogin code required (Check emails)"); +} + +void MFAScreen_SetActive(void) { + struct MFAScreen* s = &MFAScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = mfa_widgets; + s->maxWidgets = Array_Elems(mfa_widgets); + + s->Activated = MFAScreen_Activated; + s->title = "Enter login code"; + s->onEnterWidget = (struct LWidget*)&s->btnSignIn; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*----------------------------------------------------------SplitScreen----------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_SPLITSCREEN +static struct SplitScreen { + LScreen_Layout + struct LButton btnPlayers[3], btnBack; + cc_bool signingIn; +} SplitScreen; + +#define SPLITSCREEN_MAX_WIDGETS 4 +static struct LWidget* split_widgets[SPLITSCREEN_MAX_WIDGETS]; + +LAYOUTS sps_btnPlayers2[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -120 } }; +LAYOUTS sps_btnPlayers3[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -70 } }; +LAYOUTS sps_btnPlayers4[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -20 } }; +LAYOUTS sps_btnBack[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 170 } }; + +static void SplitScreen_Start(int players) { + static const cc_string user = String_FromConst(DEFAULT_USERNAME); + Game_NumLocalPlayers = players; + + Launcher_StartGame(&user, &String_Empty, &String_Empty, &String_Empty, &String_Empty); +} + +static void SplitScreen_Players2(void* w) { SplitScreen_Start(2); } +static void SplitScreen_Players3(void* w) { SplitScreen_Start(3); } +static void SplitScreen_Players4(void* w) { SplitScreen_Start(4); } + +static void SplitScreen_Activated(struct LScreen* s_) { + struct SplitScreen* s = (struct SplitScreen*)s_; + + LButton_Add(s, &s->btnPlayers[0], 300, 35, "2 player splitscreen", + SplitScreen_Players2, sps_btnPlayers2); + LButton_Add(s, &s->btnPlayers[1], 300, 35, "3 player splitscreen", + SplitScreen_Players3, sps_btnPlayers3); + LButton_Add(s, &s->btnPlayers[2], 300, 35, "4 player splitscreen", + SplitScreen_Players4, sps_btnPlayers4); + + LButton_Add(s, &s->btnBack, 100, 35, "Back", + SwitchToMain, sps_btnBack); +} + +void SplitScreen_SetActive(void) { + struct SplitScreen* s = &SplitScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = split_widgets; + s->maxWidgets = Array_Elems(split_widgets); + + s->Activated = SplitScreen_Activated; + s->title = "Splitscreen mode"; + + Launcher_SetScreen((struct LScreen*)s); +} + +static void SwitchToSplitScreen(void* w) { SplitScreen_SetActive(); } +#endif + + +/*########################################################################################################################* +*----------------------------------------------------------MainScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static struct MainScreen { + LScreen_Layout + struct LButton btnLogin, btnResume, btnDirect, btnSPlayer, btnSplit; + struct LButton btnRegister, btnOptions, btnUpdates; + struct LInput iptUsername, iptPassword; + struct LLabel lblStatus, lblUpdate; + cc_bool signingIn; +} MainScreen; + +#define MAINSCREEN_MAX_WIDGETS 12 +static struct LWidget* main_widgets[MAINSCREEN_MAX_WIDGETS]; + +LAYOUTS main_iptUsername[] = { { ANCHOR_CENTRE_MIN, -140 }, { ANCHOR_CENTRE, -120 } }; +LAYOUTS main_iptPassword[] = { { ANCHOR_CENTRE_MIN, -140 }, { ANCHOR_CENTRE, -75 } }; + +LAYOUTS main_btnLogin[] = { { ANCHOR_CENTRE, -90 }, { ANCHOR_CENTRE, -25 } }; +LAYOUTS main_lblStatus[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 20 } }; + +LAYOUTS main_btnResume[] = { { ANCHOR_CENTRE, 90 }, { ANCHOR_CENTRE, -25 } }; +LAYOUTS main_btnDirect[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 60 } }; +LAYOUTS main_btnSPlayer[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 110 } }; +LAYOUTS main_btnSplit[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 160 } }; + +LAYOUTS main_btnRegister[] = { { ANCHOR_MIN, 6 }, { ANCHOR_MAX, 6 } }; +LAYOUTS main_btnOptions[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_MAX, 6 } }; +LAYOUTS main_btnUpdates[] = { { ANCHOR_MAX, 6 }, { ANCHOR_MAX, 6 } }; + +LAYOUTS main_lblUpdate_N[] = { { ANCHOR_MAX, 10 }, { ANCHOR_MAX, 45 } }; +LAYOUTS main_lblUpdate_H[] = { { ANCHOR_MAX, 10 }, { ANCHOR_MAX, 6 } }; + + +struct ResumeInfo { + cc_string user, ip, port, server, mppass; + char _userBuffer[STRING_SIZE], _serverBuffer[STRING_SIZE]; + char _ipBuffer[16], _portBuffer[16], _mppassBuffer[STRING_SIZE]; + cc_bool valid; +}; + +CC_NOINLINE static void MainScreen_GetResume(struct ResumeInfo* info, cc_bool full) { + String_InitArray(info->server, info->_serverBuffer); + Options_Get(ROPT_SERVER, &info->server, ""); + String_InitArray(info->user, info->_userBuffer); + Options_Get(ROPT_USER, &info->user, ""); + + String_InitArray(info->ip, info->_ipBuffer); + Options_Get(ROPT_IP, &info->ip, ""); + String_InitArray(info->port, info->_portBuffer); + Options_Get(ROPT_PORT, &info->port, ""); + + if (!full) return; + String_InitArray(info->mppass, info->_mppassBuffer); + Options_GetSecure(ROPT_MPPASS, &info->mppass); + + info->valid = + info->user.length && info->mppass.length && + info->ip.length && info->port.length; +} + +CC_NOINLINE static void MainScreen_Error(struct HttpRequest* req, const char* action) { + cc_string str; char strBuffer[STRING_SIZE]; + struct MainScreen* s = &MainScreen; + String_InitArray(str, strBuffer); + + Launcher_DisplayHttpError(req, action, &str); + LLabel_SetText(&s->lblStatus, &str); + s->signingIn = false; +} + +static void MainScreen_DoLogin(void) { + struct MainScreen* s = &MainScreen; + cc_string* user = &s->iptUsername.text; + cc_string* pass = &s->iptPassword.text; + + if (!user->length) { + LLabel_SetConst(&s->lblStatus, "&cUsername required"); return; + } + if (!pass->length) { + LLabel_SetConst(&s->lblStatus, "&cPassword required"); return; + } + + if (GetTokenTask.Base.working) return; + Options_Set(LOPT_USERNAME, user); + Options_SetSecure(LOPT_PASSWORD, pass); + + GetTokenTask_Run(); + LLabel_SetConst(&s->lblStatus, "&eSigning in.."); + s->signingIn = true; +} +static void MainScreen_Login(void* w) { MainScreen_DoLogin(); } + +static void MainScreen_Register(void* w) { + static const cc_string regUrl = String_FromConst(REGISTERNEW_URL); + cc_result res = Process_StartOpen(®Url); + if (res) Logger_SimpleWarn(res, "opening register webpage in browser"); +} + +static void MainScreen_Resume(void* w) { + struct ResumeInfo info; + MainScreen_GetResume(&info, true); + + if (!info.valid) return; + Launcher_StartGame(&info.user, &info.mppass, &info.ip, &info.port, &info.server); +} + +static void MainScreen_Singleplayer(void* w) { + static const cc_string defUser = String_FromConst(DEFAULT_USERNAME); + const cc_string* user = &MainScreen.iptUsername.text; + + if (!user->length) user = &defUser; + Launcher_StartGame(user, &String_Empty, &String_Empty, &String_Empty, &String_Empty); +} + +static void MainScreen_ResumeHover(void* w) { + cc_string str; char strBuffer[STRING_SIZE]; + struct MainScreen* s = &MainScreen; + struct ResumeInfo info; + if (s->signingIn) return; + + MainScreen_GetResume(&info, false); + if (!info.user.length) return; + String_InitArray(str, strBuffer); + + if (info.server.length && String_Equals(&info.user, &s->iptUsername.text)) { + String_Format1(&str, "&eResume to %s", &info.server); + } else if (info.server.length) { + String_Format2(&str, "&eResume as %s to %s", &info.user, &info.server); + } else { + String_Format3(&str, "&eResume as %s to %s:%s", &info.user, &info.ip, &info.port); + } + LLabel_SetText(&s->lblStatus, &str); +} + +static void MainScreen_ResumeUnhover(void* w) { + struct MainScreen* s = &MainScreen; + if (s->signingIn) return; + + LLabel_SetConst(&s->lblStatus, ""); +} + +CC_NOINLINE static cc_uint32 MainScreen_GetVersion(const cc_string* version) { + cc_uint8 raw[4] = { 0, 0, 0, 0 }; + cc_string parts[4]; + int i, count; + + /* 1.0.1 -> { 1, 0, 1, 0 } */ + count = String_UNSAFE_Split(version, '.', parts, 4); + for (i = 0; i < count; i++) + { + Convert_ParseUInt8(&parts[i], &raw[i]); + } + return Stream_GetU32_BE(raw); +} + +static void MainScreen_ApplyUpdateLabel(struct MainScreen* s) { + static const cc_string currentStr = String_FromConst(GAME_APP_VER); + cc_uint32 latest, current; + + if (CheckUpdateTask.Base.success) { + latest = MainScreen_GetVersion(&CheckUpdateTask.latestRelease); + current = MainScreen_GetVersion(¤tStr); +#ifdef CC_BUILD_FLATPAK + LLabel_SetConst(&s->lblUpdate, false ? "&aUpdate available" : "&eUp to date"); +#else + LLabel_SetConst(&s->lblUpdate, false ? "&aNew release" : "&eUp to date"); +#endif + } else { + LLabel_SetConst(&s->lblUpdate, "&cCheck failed"); + } +} + +#ifdef CC_BUILD_CONSOLE +static void MainScreen_ExitApp(void* w) { + Window_Main.Exists = false; +} +#endif + +static void MainScreen_Activated(struct LScreen* s_) { + struct MainScreen* s = (struct MainScreen*)s_; + + s->iptPassword.inputType = KEYBOARD_TYPE_PASSWORD; + s->lblUpdate.small = true; + +#ifdef CC_BUILD_NETWORKING + LInput_Add(s, &s->iptUsername, 280, "Username..", main_iptUsername); + LInput_Add(s, &s->iptPassword, 280, "Password..", main_iptPassword); + LButton_Add(s, &s->btnLogin, 100, 35, "Sign in", + MainScreen_Login, main_btnLogin); + LButton_Add(s, &s->btnResume, 100, 35, "Resume", + MainScreen_Resume, main_btnResume); +#endif + + LLabel_Add(s, &s->lblStatus, "", main_lblStatus); +#ifdef CC_BUILD_NETWORKING + LButton_Add(s, &s->btnDirect, 200, 35, "Direct connect", + SwitchToDirectConnect, main_btnDirect); +#endif + LButton_Add(s, &s->btnSPlayer, 200, 35, "Singleplayer", + MainScreen_Singleplayer, main_btnSPlayer); +#ifdef CC_BUILD_SPLITSCREEN + LButton_Add(s, &s->btnSplit, 200, 35, "Splitscreen (WIP)", + SwitchToSplitScreen, main_btnSplit); +#endif + + if (Process_OpenSupported) { + LButton_Add(s, &s->btnRegister, 100, 35, "Register", + MainScreen_Register, main_btnRegister); + } + + LButton_Add(s, &s->btnOptions, 100, 35, "Options", + SwitchToSettings, main_btnOptions); + +#ifdef CC_BUILD_CONSOLE + LLabel_Add(s, &s->lblUpdate, "&eChecking..", main_lblUpdate_N); + LButton_Add(s, &s->btnUpdates, 100, 35, "Exit", + MainScreen_ExitApp, main_btnUpdates); +#else + LLabel_Add(s, &s->lblUpdate, "&eChecking..", + Updater_Supported ? main_lblUpdate_N : main_lblUpdate_H); + if (Updater_Supported) { + LButton_Add(s, &s->btnUpdates, 100, 35, "Updates", + SwitchToUpdates, main_btnUpdates); + } +#endif + + s->btnResume.OnHover = MainScreen_ResumeHover; + s->btnResume.OnUnhover = MainScreen_ResumeUnhover; + + if (CheckUpdateTask.Base.completed) + MainScreen_ApplyUpdateLabel(s); +} + +static void MainScreen_Load(struct LScreen* s_) { + cc_string user, pass; char passBuffer[STRING_SIZE]; + struct MainScreen* s = (struct MainScreen*)s_; + + String_InitArray(pass, passBuffer); + Options_UNSAFE_Get(LOPT_USERNAME, &user); + Options_GetSecure(LOPT_PASSWORD, &pass); + + LInput_SetText(&s->iptUsername, &user); + LInput_SetText(&s->iptPassword, &pass); + + /* Auto sign in when automatically joining a server */ + if (!Launcher_AutoHash.length) return; + if (!user.length || !pass.length) return; + MainScreen_DoLogin(); +} + +static void MainScreen_TickCheckUpdates(struct MainScreen* s) { + if (!CheckUpdateTask.Base.working) return; + LWebTask_Tick(&CheckUpdateTask.Base, NULL); + + if (!CheckUpdateTask.Base.completed) return; + MainScreen_ApplyUpdateLabel(s); +} + +static void MainScreen_LoginPhase2(struct MainScreen* s, const cc_string* user) { + /* website returns case correct username */ + if (!String_Equals(&s->iptUsername.text, user)) { + LInput_SetText(&s->iptUsername, user); + } + String_Copy(&Launcher_Username, user); + + FetchServersTask_Run(); + LLabel_SetConst(&s->lblStatus, "&eRetrieving servers list.."); +} + +static void MainScreen_SignInError(struct HttpRequest* req) { + MainScreen_Error(req, "signing in"); +} + +static void MainScreen_TickGetToken(struct MainScreen* s) { + if (!GetTokenTask.Base.working) return; + LWebTask_Tick(&GetTokenTask.Base, MainScreen_SignInError); + + if (!GetTokenTask.Base.completed) return; + if (!GetTokenTask.Base.success) return; + + if (!GetTokenTask.error && String_CaselessEquals(&GetTokenTask.username, &s->iptUsername.text)) { + /* Already logged in, go straight to fetching servers */ + MainScreen_LoginPhase2(s, &GetTokenTask.username); + } else { + SignInTask_Run(&s->iptUsername.text, &s->iptPassword.text, + &MFAScreen.iptCode.text); + } +} + +static void MainScreen_TickSignIn(struct MainScreen* s) { + if (!SignInTask.Base.working) return; + LWebTask_Tick(&SignInTask.Base, MainScreen_SignInError); + if (!SignInTask.Base.completed) return; + + if (SignInTask.needMFA) { MFAScreen_SetActive(); return; } + + if (SignInTask.error) { + LLabel_SetConst(&s->lblStatus, SignInTask.error); + } else if (SignInTask.Base.success) { + MainScreen_LoginPhase2(s, &SignInTask.username); + } +} + +static void MainScreen_ServersError(struct HttpRequest* req) { + MainScreen_Error(req, "retrieving servers list"); +} + +static void MainScreen_TickFetchServers(struct MainScreen* s) { + if (!FetchServersTask.Base.working) return; + LWebTask_Tick(&FetchServersTask.Base, MainScreen_ServersError); + + if (!FetchServersTask.Base.completed) return; + if (!FetchServersTask.Base.success) return; + + s->signingIn = false; + if (Launcher_AutoHash.length) { + Launcher_ConnectToServer(&Launcher_AutoHash); + Launcher_AutoHash.length = 0; + } else { + ServersScreen_SetActive(); + } +} + +static void MainScreen_Tick(struct LScreen* s_) { + struct MainScreen* s = (struct MainScreen*)s_; + LScreen_Tick(s_); + + MainScreen_TickCheckUpdates(s); + MainScreen_TickGetToken(s); + MainScreen_TickSignIn(s); + MainScreen_TickFetchServers(s); +} + +void MainScreen_SetActive(void) { + struct MainScreen* s = &MainScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = main_widgets; + s->maxWidgets = Array_Elems(main_widgets); + + s->Activated = MainScreen_Activated; + s->LoadState = MainScreen_Load; + s->Tick = MainScreen_Tick; + s->title = "ClassiHax"; + +#ifdef CC_BUILD_NETWORKING + s->onEnterWidget = (struct LWidget*)&s->btnLogin; +#else + s->onEnterWidget = (struct LWidget*)&s->btnSPlayer; +#endif + + Launcher_SetScreen((struct LScreen*)s); +} + + +#ifdef CC_BUILD_RESOURCES +/*########################################################################################################################* +*----------------------------------------------------CheckResourcesScreen-------------------------------------------------* +*#########################################################################################################################*/ +static struct CheckResourcesScreen { + LScreen_Layout + struct LLabel lblLine1, lblLine2, lblStatus; + struct LButton btnYes, btnNo; +} CheckResourcesScreen; + +#define CHECKRESOURCES_SCREEN_MAX_WIDGET 5 +static struct LWidget* checkResources_widgets[CHECKRESOURCES_SCREEN_MAX_WIDGET]; + +LAYOUTS cres_lblLine1[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -50 } }; +LAYOUTS cres_lblLine2[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -30 } }; +LAYOUTS cres_lblStatus[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 10 } }; + +LAYOUTS cres_btnYes[] = { { ANCHOR_CENTRE, -70 }, { ANCHOR_CENTRE, 45 } }; +LAYOUTS cres_btnNo[] = { { ANCHOR_CENTRE, 70 }, { ANCHOR_CENTRE, 45 } }; + + +static void CheckResourcesScreen_Yes(void* w) { FetchResourcesScreen_SetActive(); } +static void CheckResourcesScreen_Next(void* w) { + Http_ClearPending(); + if (Options_LoadResult != ReturnCode_FileNotFound) { + MainScreen_SetActive(); + } else { + ChooseModeScreen_SetActive(true); + } +} + +static void CheckResourcesScreen_AddWidgets(struct CheckResourcesScreen* s) { + const char* line1_msg = Resources_MissingRequired ? "Some required resources weren't found" + : "Some optional resources weren't found"; + s->lblStatus.small = true; + + LLabel_Add(s, &s->lblLine1, line1_msg, cres_lblLine1); + LLabel_Add(s, &s->lblLine2, "Okay to download?", cres_lblLine2); + LLabel_Add(s, &s->lblStatus, "", cres_lblStatus); + + LButton_Add(s, &s->btnYes, 70, 35, "Yes", + CheckResourcesScreen_Yes, cres_btnYes); + LButton_Add(s, &s->btnNo, 70, 35, "No", + CheckResourcesScreen_Next, cres_btnNo); +} + +static void CheckResourcesScreen_Activated(struct LScreen* s_) { + struct CheckResourcesScreen* s = (struct CheckResourcesScreen*)s_; + cc_string str; char buffer[STRING_SIZE]; + float size; + CheckResourcesScreen_AddWidgets(s); + + size = Resources_MissingSize / 1024.0f; + String_InitArray(str, buffer); + String_Format1(&str, "&eDownload size: %f2 megabytes", &size); + LLabel_SetText(&s->lblStatus, &str); +} + +static void CheckResourcesScreen_ResetArea(struct Context2D* ctx, int x, int y, int width, int height) { + int R = BitmapCol_R(Launcher_Theme.BackgroundColor) * 0.78f; /* 153 -> 120 */ + int G = BitmapCol_G(Launcher_Theme.BackgroundColor) * 0.70f; /* 127 -> 89 */ + int B = BitmapCol_B(Launcher_Theme.BackgroundColor) * 0.88f; /* 172 -> 151 */ + + Gradient_Noise(ctx, BitmapColor_RGB(R, G, B), 4, x, y, width, height); +} + +static void CheckResourcesScreen_DrawBackground(struct LScreen* s, struct Context2D* ctx) { + int x, y, width, height; + BitmapCol color = BitmapColor_Scale(Launcher_Theme.BackgroundColor, 0.2f); + Context2D_Clear(ctx, color, 0, 0, ctx->width, ctx->height); + width = Display_ScaleX(380); + height = Display_ScaleY(140); + + x = Gui_CalcPos(ANCHOR_CENTRE, 0, width, ctx->width); + y = Gui_CalcPos(ANCHOR_CENTRE, 0, height, ctx->height); + CheckResourcesScreen_ResetArea(ctx, x, y, width, height); +} + +void CheckResourcesScreen_SetActive(void) { + struct CheckResourcesScreen* s = &CheckResourcesScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = checkResources_widgets; + s->maxWidgets = Array_Elems(checkResources_widgets); + + s->Activated = CheckResourcesScreen_Activated; + s->DrawBackground = CheckResourcesScreen_DrawBackground; + s->ResetArea = CheckResourcesScreen_ResetArea; + s->onEnterWidget = (struct LWidget*)&s->btnYes; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*----------------------------------------------------FetchResourcesScreen-------------------------------------------------* +*#########################################################################################################################*/ +static struct FetchResourcesScreen { + LScreen_Layout + struct LLabel lblStatus; + struct LButton btnCancel; + struct LSlider sdrProgress; +} FetchResourcesScreen; + +#define FETCHRESOURCES_SCREEN_MAX_WIDGETS 3 +static struct LWidget* fetchResources_widgets[FETCHRESOURCES_SCREEN_MAX_WIDGETS]; + +LAYOUTS fres_lblStatus[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -10 } }; +LAYOUTS fres_btnCancel[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 45 } }; +LAYOUTS fres_sdrProgress[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 15 } }; + + +static void FetchResourcesScreen_Error(struct HttpRequest* req) { + cc_string str; char buffer[STRING_SIZE]; + String_InitArray(str, buffer); + + Launcher_DisplayHttpError(req, "downloading resources", &str); + LLabel_SetText(&FetchResourcesScreen.lblStatus, &str); +} + +static void FetchResourcesScreen_Activated(struct LScreen* s_) { + struct FetchResourcesScreen* s = (struct FetchResourcesScreen*)s_; + s->lblStatus.small = true; + + LLabel_Add(s, &s->lblStatus, "", fres_lblStatus); + LButton_Add(s, &s->btnCancel, 120, 35, "Cancel", + CheckResourcesScreen_Next, fres_btnCancel); + LSlider_Add(s, &s->sdrProgress, 200, 12, BitmapColor_RGB(0, 220, 0), fres_sdrProgress); + + Fetcher_ErrorCallback = FetchResourcesScreen_Error; + Fetcher_Run(); +} + +static void FetchResourcesScreen_UpdateStatus(struct FetchResourcesScreen* s, int reqID) { + cc_string str; char strBuffer[STRING_SIZE]; + const char* name; + int count; + + name = Fetcher_RequestName(reqID); + if (!name) return; + + String_InitArray(str, strBuffer); + count = Fetcher_Downloaded + 1; + String_Format3(&str, "&eFetching %c.. (%i/%i)", name, &count, &Resources_MissingCount); + + if (String_Equals(&str, &s->lblStatus.text)) return; + LLabel_SetText(&s->lblStatus, &str); +} + +static void FetchResourcesScreen_UpdateProgress(struct FetchResourcesScreen* s) { + int reqID, progress; + + if (!Http_GetCurrent(&reqID, &progress)) return; + FetchResourcesScreen_UpdateStatus(s, reqID); + /* making request still, haven't started download yet */ + if (progress < 0 || progress > 100) return; + + LSlider_SetProgress(&s->sdrProgress, progress); +} + +static void FetchResourcesScreen_Tick(struct LScreen* s_) { + struct FetchResourcesScreen* s = (struct FetchResourcesScreen*)s_; + if (!Fetcher_Working) return; + + FetchResourcesScreen_UpdateProgress(s); + Fetcher_Update(); + + if (!Fetcher_Completed) return; + if (Fetcher_Failed) return; + + Launcher_TryLoadTexturePack(); + CheckResourcesScreen_Next(NULL); +} + +void FetchResourcesScreen_SetActive(void) { + struct FetchResourcesScreen* s = &FetchResourcesScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = fetchResources_widgets; + s->maxWidgets = Array_Elems(fetchResources_widgets); + + s->Activated = FetchResourcesScreen_Activated; + s->Tick = FetchResourcesScreen_Tick; + s->DrawBackground = CheckResourcesScreen_DrawBackground; + s->ResetArea = CheckResourcesScreen_ResetArea; + + Launcher_SetScreen((struct LScreen*)s); +} + +#endif + +/*########################################################################################################################* +*--------------------------------------------------------ServersScreen----------------------------------------------------* +*#########################################################################################################################*/ +static struct ServersScreen { + LScreen_Layout + struct LInput iptSearch, iptHash; + struct LButton btnBack, btnConnect, btnRefresh; + struct LTable table; + struct FontDesc rowFont; + float tableAcc; +} ServersScreen; + +static struct LWidget* servers_widgets[6]; + +LAYOUTS srv_iptSearch[] = { { ANCHOR_MIN, 10 }, { ANCHOR_MIN, 10 } }; +LAYOUTS srv_iptHash[] = { { ANCHOR_MIN, 10 }, { ANCHOR_MAX, 10 } }; +LAYOUTS srv_table[5] = { { ANCHOR_MIN, 10 }, { ANCHOR_MIN | LLAYOUT_EXTRA, 50 }, { LLAYOUT_WIDTH, 0 }, { LLAYOUT_HEIGHT, 50 } }; + +LAYOUTS srv_btnBack[] = { { ANCHOR_MAX, 10 }, { ANCHOR_MIN, 10 } }; +LAYOUTS srv_btnConnect[] = { { ANCHOR_MAX, 10 }, { ANCHOR_MAX, 10 } }; +LAYOUTS srv_btnRefresh[] = { { ANCHOR_MAX, 135 }, { ANCHOR_MIN, 10 } }; + + +static void ServersScreen_Connect(void* w) { + struct LTable* table = &ServersScreen.table; + cc_string* hash = &ServersScreen.iptHash.text; + + if (!hash->length && table->rowsCount) { + hash = <able_Get(table->topRow)->hash; + } + Launcher_ConnectToServer(hash); +} + +static void ServersScreen_Refresh(void* w) { + struct LButton* btn; + if (FetchServersTask.Base.working) return; + + FetchServersTask_Run(); + btn = &ServersScreen.btnRefresh; + LButton_SetConst(btn, "&eWorking.."); +} + +static void ServersScreen_HashFilter(cc_string* str) { + int lastIndex; + if (!str->length) return; + + /* Server url look like http://www.classicube.net/server/play/aaaaa/ */ + /* Trim it to only be the aaaaa */ + if (str->buffer[str->length - 1] == '/') str->length--; + + /* Trim the URL parts before the hash */ + lastIndex = String_LastIndexOf(str, '/'); + if (lastIndex == -1) return; + *str = String_UNSAFE_SubstringAt(str, lastIndex + 1); +} + +static void ServersScreen_SearchChanged(struct LInput* w) { + struct ServersScreen* s = &ServersScreen; + LTable_ApplyFilter(&s->table); + LBackend_MarkDirty(&s->table); +} + +static void ServersScreen_HashChanged(struct LInput* w) { + struct ServersScreen* s = &ServersScreen; + LTable_ShowSelected(&s->table); + LBackend_MarkDirty(&s->table); +} + +static void ServersScreen_OnSelectedChanged(void) { + struct ServersScreen* s = &ServersScreen; + LBackend_MarkDirty(&s->iptHash); + LBackend_MarkDirty(&s->table); +} + +static void ServersScreen_ReloadServers(struct ServersScreen* s) { + int i; + LTable_Sort(&s->table); + + for (i = 0; i < FetchServersTask.numServers; i++) + { + FetchFlagsTask_Add(&FetchServersTask.servers[i]); + } +} + +static void ServersScreen_AddWidgets(struct ServersScreen* s) { + LInput_Add(s, &s->iptSearch, 370, "Search servers..", srv_iptSearch); + LInput_Add(s, &s->iptHash, 475, "classicube.net/server/play/...", srv_iptHash); + + LButton_Add(s, &s->btnBack, 110, 30, "Back", + SwitchToMain, srv_btnBack); + LButton_Add(s, &s->btnConnect, 130, 30, "Connect", + ServersScreen_Connect, srv_btnConnect); + LButton_Add(s, &s->btnRefresh, 110, 30, "Refresh", + ServersScreen_Refresh, srv_btnRefresh); + + s->iptSearch.skipsEnter = true; + s->iptSearch.TextChanged = ServersScreen_SearchChanged; + s->iptHash.TextChanged = ServersScreen_HashChanged; + s->iptHash.ClipboardFilter = ServersScreen_HashFilter; + + s->table.filter = &s->iptSearch.text; + s->table.selectedHash = &s->iptHash.text; + s->table.OnSelectedChanged = ServersScreen_OnSelectedChanged; + + if (s->table.VTABLE) { + LScreen_AddWidget(s, &s->table); + } else { + LTable_Add(s, &s->table, srv_table); + } + LTable_Reset(&s->table); +} + +static void ServersScreen_Activated(struct LScreen* s_) { + struct ServersScreen* s = (struct ServersScreen*)s_; + ServersScreen_AddWidgets(s); + s->tableAcc = 0.0f; + + LInput_ClearText(&s->iptHash); + LInput_ClearText(&s->iptSearch); + + ServersScreen_ReloadServers(s); + /* This is so typing on keyboard by default searchs server list */ + /* But don't do that when it would cause on-screen keyboard to show */ + if (Window_Main.SoftKeyboard) return; + LScreen_SelectWidget(s_, 0, (struct LWidget*)&s->iptSearch, false); +} + +static void ServersScreen_Tick(struct LScreen* s_) { + struct ServersScreen* s = (struct ServersScreen*)s_; + LScreen_Tick(s_); + LWebTask_Tick(&FetchFlagsTask.Base, NULL); + + if (!FetchServersTask.Base.working) return; + LWebTask_Tick(&FetchServersTask.Base, NULL); + if (!FetchServersTask.Base.completed) return; + + if (FetchServersTask.Base.success) { + ServersScreen_ReloadServers(s); + LBackend_MarkDirty(&s->table); + } + + LButton_SetConst(&s->btnRefresh, + FetchServersTask.Base.success ? "Refresh" : "&cFailed"); +} + +static void ServersScreen_MouseWheel(struct LScreen* s_, float delta) { + struct ServersScreen* s = (struct ServersScreen*)s_; + s->table.VTABLE->MouseWheel(&s->table, delta); +} + +static void ServersScreen_KeyDown(struct LScreen* s_, int key, cc_bool was) { + struct ServersScreen* s = (struct ServersScreen*)s_; + if (!LTable_HandlesKey(key)) { + LScreen_KeyDown(s_, key, was); + } else { + s->table.VTABLE->KeyDown(&s->table, key, was); + } +} + +static void ServersScreen_MouseUp(struct LScreen* s_, int idx) { + struct ServersScreen* s = (struct ServersScreen*)s_; + s->table.VTABLE->OnUnselect(&s->table, idx); +} + +void ServersScreen_SetActive(void) { + struct ServersScreen* s = &ServersScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = servers_widgets; + s->maxWidgets = Array_Elems(servers_widgets); + + s->Activated = ServersScreen_Activated; + s->Tick = ServersScreen_Tick; + s->MouseWheel = ServersScreen_MouseWheel; + s->KeyDown = ServersScreen_KeyDown; + s->MouseUp = ServersScreen_MouseUp; + + s->onEnterWidget = (struct LWidget*)&s->btnConnect; + s->onEscapeWidget = (struct LWidget*)&s->btnBack; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*--------------------------------------------------------SettingsScreen---------------------------------------------------* +*#########################################################################################################################*/ +static struct SettingsScreen { + LScreen_Layout + struct LButton btnMode, btnColours, btnBack; + struct LLabel lblMode, lblColours; + struct LCheckbox cbExtra, cbEmpty, cbScale; + struct LLine sep; +} SettingsScreen; + +#define SETTINGS_SCREEN_MAX_WIDGETS 9 +static struct LWidget* settings_widgets[SETTINGS_SCREEN_MAX_WIDGETS]; + +LAYOUTS set_btnMode[] = { { ANCHOR_CENTRE, -135 }, { ANCHOR_CENTRE, -70 } }; +LAYOUTS set_lblMode[] = { { ANCHOR_CENTRE_MIN, -70 }, { ANCHOR_CENTRE, -70 } }; +LAYOUTS set_btnColours[] = { { ANCHOR_CENTRE, -135 }, { ANCHOR_CENTRE, -20 } }; +LAYOUTS set_lblColours[] = { { ANCHOR_CENTRE_MIN, -70 }, { ANCHOR_CENTRE, -20 } }; + +LAYOUTS set_sep[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 15 } }; +LAYOUTS set_cbExtra[] = { { ANCHOR_CENTRE_MIN, -190 }, { ANCHOR_CENTRE, 44 } }; +LAYOUTS set_cbEmpty[] = { { ANCHOR_CENTRE_MIN, -190 }, { ANCHOR_CENTRE, 84 } }; +LAYOUTS set_cbScale[] = { { ANCHOR_CENTRE_MIN, -190 }, { ANCHOR_CENTRE, 124 } }; +LAYOUTS set_btnBack[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 170 } }; + + +#if defined CC_BUILD_MOBILE +static void SettingsScreen_LockOrientation(struct LCheckbox* w) { + Options_SetBool(OPT_LANDSCAPE_MODE, w->value); + Window_LockLandscapeOrientation(w->value); + LBackend_Redraw(); +} +#else +static void SettingsScreen_AutoClose(struct LCheckbox* w) { + Options_SetBool(LOPT_AUTO_CLOSE, w->value); +} +#endif +static void SettingsScreen_ShowEmpty(struct LCheckbox* w) { + Launcher_ShowEmptyServers = w->value; + Options_SetBool(LOPT_SHOW_EMPTY, w->value); +} + +static void SettingsScreen_DPIScaling(struct LCheckbox* w) { +#if defined CC_BUILD_WIN + DisplayInfo.DPIScaling = w->value; + Options_SetBool(OPT_DPI_SCALING, w->value); + Window_ShowDialog("Restart required", "You must restart ClassiCube before display scaling takes effect"); +#else + Window_ShowDialog("Restart required", "Display scaling is currently only supported on Windows"); +#endif +} + +static void SettingsScreen_AddWidgets(struct SettingsScreen* s) { + LLine_Add(s, &s->sep, 380, set_sep); + LButton_Add(s, &s->btnMode, 110, 35, "Mode", + SwitchToChooseMode, set_btnMode); + LLabel_Add(s, &s->lblMode, "&eChange the enabled features", set_lblMode); + + if (!Options_GetBool(OPT_CLASSIC_MODE, false)) { + LButton_Add(s, &s->btnColours, 110, 35, "Theme", + SwitchToThemes, set_btnColours); + LLabel_Add(s, &s->lblColours, "&eChange how the launcher looks", set_lblColours); + } + +#if defined CC_BUILD_MOBILE + LCheckbox_Add(s, &s->cbExtra, "Force landscape", + SettingsScreen_LockOrientation, set_cbExtra); +#else + LCheckbox_Add(s, &s->cbExtra, "Close this after game starts", + SettingsScreen_AutoClose, set_cbExtra); +#endif + + LCheckbox_Add(s, &s->cbEmpty, "Show empty servers in list", + SettingsScreen_ShowEmpty, set_cbEmpty); + LCheckbox_Add(s, &s->cbScale, "Use display scaling", + SettingsScreen_DPIScaling, set_cbScale); + LButton_Add(s, &s->btnBack, 80, 35, "Back", + SwitchToMain, set_btnBack); +} + +static void SettingsScreen_Activated(struct LScreen* s_) { + struct SettingsScreen* s = (struct SettingsScreen*)s_; + SettingsScreen_AddWidgets(s); + +#if defined CC_BUILD_MOBILE + LCheckbox_Set(&s->cbExtra, Options_GetBool(OPT_LANDSCAPE_MODE, false)); +#else + LCheckbox_Set(&s->cbExtra, Options_GetBool(LOPT_AUTO_CLOSE, false)); +#endif + + LCheckbox_Set(&s->cbEmpty, Launcher_ShowEmptyServers); + LCheckbox_Set(&s->cbScale, DisplayInfo.DPIScaling); +} + +void SettingsScreen_SetActive(void) { + struct SettingsScreen* s = &SettingsScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = settings_widgets; + s->maxWidgets = Array_Elems(settings_widgets); + + s->Activated = SettingsScreen_Activated; + s->title = "Options"; + s->onEscapeWidget = (struct LWidget*)&s->btnBack; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*----------------------------------------------------------ThemesScreen----------------------------------------------------* +*#########################################################################################################################*/ +static struct ThemesScreen { + LScreen_Layout + struct LButton btnModern, btnClassic, btnNordic; + struct LButton btnCustom, btnBack; +} ThemesScreen; + +#define THEME_SCREEN_MAX_WIDGETS 5 +static struct LWidget* themes_widgets[THEME_SCREEN_MAX_WIDGETS]; + +LAYOUTS the_btnModern[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -120 } }; +LAYOUTS the_btnClassic[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -70 } }; +LAYOUTS the_btnNordic[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -20 } }; +LAYOUTS the_btnCustom[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 120 } }; +LAYOUTS the_btnBack[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 170 } }; + + +static void ThemesScreen_Set(const struct LauncherTheme* theme) { + Launcher_Theme = *theme; + Launcher_SaveTheme(); + LBackend_ThemeChanged(); +} + +static void ThemesScreen_Modern(void* w) { + ThemesScreen_Set(&Launcher_ModernTheme); +} +static void ThemesScreen_Classic(void* w) { + ThemesScreen_Set(&Launcher_ClassicTheme); +} +static void ThemesScreen_Nordic(void* w) { + ThemesScreen_Set(&Launcher_NordicTheme); +} + +static void ThemesScreen_Activated(struct LScreen* s_) { + struct ThemesScreen* s = (struct ThemesScreen*)s_; + + LButton_Add(s, &s->btnModern, 200, 35, "Modern", + ThemesScreen_Modern, the_btnModern); + LButton_Add(s, &s->btnClassic, 200, 35, "Classic", + ThemesScreen_Classic, the_btnClassic); + LButton_Add(s, &s->btnNordic, 200, 35, "Nordic", + ThemesScreen_Nordic, the_btnNordic); + LButton_Add(s, &s->btnCustom, 200, 35, "Custom", + SwitchToColours, the_btnCustom); + LButton_Add(s, &s->btnBack, 80, 35, "Back", + SwitchToSettings, the_btnBack); +} + +void ThemesScreen_SetActive(void) { + struct ThemesScreen* s = &ThemesScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = themes_widgets; + s->maxWidgets = Array_Elems(themes_widgets); + + s->Activated = ThemesScreen_Activated; + s->title = "Select theme"; + s->onEscapeWidget = (struct LWidget*)&s->btnBack; + + Launcher_SetScreen((struct LScreen*)s); +} + + +/*########################################################################################################################* +*--------------------------------------------------------UpdatesScreen----------------------------------------------------* +*#########################################################################################################################*/ +static struct UpdatesScreen { + LScreen_Layout + struct LLine seps[2]; + struct LButton btnRel[2], btnDev[2], btnBack; + struct LLabel lblYour, lblRel, lblDev, lblInfo, lblStatus; + int buildProgress, buildIndex; + cc_bool pendingFetch, release; +} UpdatesScreen; + +#define UPDATESSCREEN_MAX_WIDGETS 12 +static struct LWidget* updates_widgets[UPDATESSCREEN_MAX_WIDGETS]; + +LAYOUTS upd_lblYour[] = { { ANCHOR_CENTRE, -5 }, { ANCHOR_CENTRE, -120 } }; +LAYOUTS upd_seps0[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -100 } }; +LAYOUTS upd_seps1[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -5 } }; + +LAYOUTS upd_lblRel[] = { { ANCHOR_CENTRE, -20 }, { ANCHOR_CENTRE, -75 } }; +LAYOUTS upd_lblDev[] = { { ANCHOR_CENTRE, -30 }, { ANCHOR_CENTRE, 20 } }; +LAYOUTS upd_lblInfo[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 105 } }; +LAYOUTS upd_lblStatus[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 130 } }; +LAYOUTS upd_btnBack[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 170 } }; + +/* Update button layouts when 1 build */ +LAYOUTS upd_btnRel0_1[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, -40 } }; +LAYOUTS upd_btnDev0_1[] = { { ANCHOR_CENTRE, 0 }, { ANCHOR_CENTRE, 55 } }; +/* Update button layouts when 2 builds */ +LAYOUTS upd_btnRel0_2[] = { { ANCHOR_CENTRE, -80 }, { ANCHOR_CENTRE, -40 } }; +LAYOUTS upd_btnRel1_2[] = { { ANCHOR_CENTRE, 80 }, { ANCHOR_CENTRE, -40 } }; +LAYOUTS upd_btnDev0_2[] = { { ANCHOR_CENTRE, -80 }, { ANCHOR_CENTRE, 55 } }; +LAYOUTS upd_btnDev1_2[] = { { ANCHOR_CENTRE, 80 }, { ANCHOR_CENTRE, 55 } }; + + +CC_NOINLINE static void UpdatesScreen_FormatTime(cc_string* str, int delta) { + const char* span; + int unit, value = Math_AbsI(delta); + + if (value < SECS_PER_MIN) { + span = "second"; unit = 1; + } else if (value < SECS_PER_HOUR) { + span = "minute"; unit = SECS_PER_MIN; + } else if (value < SECS_PER_DAY) { + span = "hour"; unit = SECS_PER_HOUR; + } else { + span = "day"; unit = SECS_PER_DAY; + } + + value /= unit; + String_AppendInt(str, value); + String_Append(str, ' '); + String_AppendConst(str, span); + + if (value > 1) String_Append(str, 's'); + String_AppendConst(str, delta >= 0 ? " ago" : " in the future"); +} + +static void UpdatesScreen_Format(struct LLabel* lbl, const char* prefix, cc_uint64 timestamp) { + cc_string str; char buffer[STRING_SIZE]; + TimeMS now; + int delta; + + String_InitArray(str, buffer); + String_AppendConst(&str, prefix); + + if (!timestamp) { + String_AppendConst(&str, "&cCheck failed"); + } else { + now = DateTime_CurrentUTC() - UNIX_EPOCH_SECONDS; + delta = (int)(now - timestamp); + UpdatesScreen_FormatTime(&str, delta); + } + LLabel_SetText(lbl, &str); +} + +static void UpdatesScreen_FormatBoth(struct UpdatesScreen* s) { + UpdatesScreen_Format(&s->lblRel, "Latest release: ", CheckUpdateTask.relTimestamp); + UpdatesScreen_Format(&s->lblDev, "Latest dev build: ", CheckUpdateTask.devTimestamp); +} + +static void UpdatesScreen_UpdateHeader(struct UpdatesScreen* s, cc_string* str) { + const char* message; + if ( s->release) message = "&eFetching latest release "; + if (!s->release) message = "&eFetching latest dev build "; + + String_Format2(str, "%c%c", message, Updater_Info.builds[s->buildIndex].name); +} + +static void UpdatesScreen_DoFetch(struct UpdatesScreen* s) { + cc_string str; char strBuffer[STRING_SIZE]; + cc_uint64 time; + + time = s->release ? CheckUpdateTask.relTimestamp : CheckUpdateTask.devTimestamp; + if (!time || FetchUpdateTask.Base.working) return; + FetchUpdateTask_Run(s->release, s->buildIndex); + + s->pendingFetch = false; + s->buildProgress = -1; + String_InitArray(str, strBuffer); + + UpdatesScreen_UpdateHeader(s, &str); + String_AppendConst(&str, ".."); + LLabel_SetText(&s->lblStatus, &str); +} + +static void UpdatesScreen_Get(cc_bool release, int buildIndex) { + struct UpdatesScreen* s = &UpdatesScreen; + /* This code is deliberately split up to handle this particular case: */ + /* The user clicked this button before CheckUpdateTask completed, */ + /* therefore update fetching would not actually occur. (as timestamp is 0) */ + /* By storing requested build, it can be fetched later upon completion. */ + s->pendingFetch = true; + s->release = release; + s->buildIndex = buildIndex; + UpdatesScreen_DoFetch(s); +} + +static void UpdatesScreen_CheckTick(struct UpdatesScreen* s) { + if (!CheckUpdateTask.Base.working) return; + LWebTask_Tick(&CheckUpdateTask.Base, NULL); + + if (!CheckUpdateTask.Base.completed) return; + UpdatesScreen_FormatBoth(s); + if (s->pendingFetch) UpdatesScreen_DoFetch(s); +} + +static void UpdatesScreen_UpdateProgress(struct UpdatesScreen* s, struct LWebTask* task) { + cc_string str; char strBuffer[STRING_SIZE]; + int progress = Http_CheckProgress(task->reqID); + if (progress == s->buildProgress) return; + + s->buildProgress = progress; + if (progress < 0 || progress > 100) return; + String_InitArray(str, strBuffer); + + UpdatesScreen_UpdateHeader(s, &str); + String_Format1(&str, " &a%i%%", &s->buildProgress); + LLabel_SetText(&s->lblStatus, &str); +} + +static void FetchUpdatesError(struct HttpRequest* req) { + cc_string str; char strBuffer[STRING_SIZE]; + String_InitArray(str, strBuffer); + + Launcher_DisplayHttpError(req, "fetching update", &str); + LLabel_SetText(&UpdatesScreen.lblStatus, &str); +} + +static void UpdatesScreen_FetchTick(struct UpdatesScreen* s) { + if (!FetchUpdateTask.Base.working) return; + + LWebTask_Tick(&FetchUpdateTask.Base, FetchUpdatesError); + UpdatesScreen_UpdateProgress(s, &FetchUpdateTask.Base); + if (!FetchUpdateTask.Base.completed) return; + + if (!FetchUpdateTask.Base.success) return; + /* FetchUpdateTask handles saving the updated file for us */ + Launcher_ShouldExit = true; + Launcher_ShouldUpdate = true; +} + +static void UpdatesScreen_Rel_0(void* w) { UpdatesScreen_Get(true, 0); } +static void UpdatesScreen_Rel_1(void* w) { UpdatesScreen_Get(true, 1); } +static void UpdatesScreen_Dev_0(void* w) { UpdatesScreen_Get(false, 0); } +static void UpdatesScreen_Dev_1(void* w) { UpdatesScreen_Get(false, 1); } + +static void UpdatesScreen_AddWidgets(struct UpdatesScreen* s) { + int builds = Updater_Info.numBuilds; + + LLine_Add(s, &s->seps[0], 320, upd_seps0); + LLine_Add(s, &s->seps[1], 320, upd_seps1); + LLabel_Add(s, &s->lblYour, "Your build: (unknown)", upd_lblYour); + + LLabel_Add(s, &s->lblRel, "Latest release: Checking..", upd_lblRel); + LLabel_Add(s, &s->lblDev, "Latest dev build: Checking..", upd_lblDev); + LLabel_Add(s, &s->lblStatus, "", upd_lblStatus); + LLabel_Add(s, &s->lblInfo, Updater_Info.info, upd_lblInfo); + LButton_Add(s, &s->btnBack, 80, 35, "Back", + SwitchToMain, upd_btnBack); + + if (builds >= 1) { + LButton_Add(s, &s->btnRel[0], 130, 35, Updater_Info.builds[0].name, + UpdatesScreen_Rel_0, builds == 1 ? upd_btnRel0_1 : upd_btnRel0_2); + LButton_Add(s, &s->btnDev[0], 130, 35, Updater_Info.builds[0].name, + UpdatesScreen_Dev_0, builds == 1 ? upd_btnDev0_1 : upd_btnDev0_2); + } + + if (builds >= 2) { + LButton_Add(s, &s->btnRel[1], 130, 35, Updater_Info.builds[1].name, + UpdatesScreen_Rel_1, upd_btnRel1_2); + LButton_Add(s, &s->btnDev[1], 130, 35, Updater_Info.builds[1].name, + UpdatesScreen_Dev_1, upd_btnDev1_2); + } +} + +static void UpdatesScreen_Activated(struct LScreen* s_) { + struct UpdatesScreen* s = (struct UpdatesScreen*)s_; + cc_uint64 buildTime; + cc_result res; + UpdatesScreen_AddWidgets(s); + + /* Initially fill out with update check result from main menu */ + if (CheckUpdateTask.Base.completed && CheckUpdateTask.Base.success) { + UpdatesScreen_FormatBoth(s); + } + CheckUpdateTask_Run(); + s->pendingFetch = false; + + res = Updater_GetBuildTime(&buildTime); + if (res) { Logger_SysWarn(res, "getting build time"); return; } + UpdatesScreen_Format(&s->lblYour, "Your build: ", buildTime); +} + +static void UpdatesScreen_Tick(struct LScreen* s_) { + struct UpdatesScreen* s = (struct UpdatesScreen*)s_; + LScreen_Tick(s_); + + UpdatesScreen_FetchTick(s); + UpdatesScreen_CheckTick(s); +} + +/* Aborts fetch if it is in progress */ +static void UpdatesScreen_Deactivated(struct LScreen* s_) { + struct UpdatesScreen* s = (struct UpdatesScreen*)s_; + s->buildProgress = -1; + + FetchUpdateTask.Base.working = false; + s->lblStatus.text.length = 0; +} + +void UpdatesScreen_SetActive(void) { + struct UpdatesScreen* s = &UpdatesScreen; + LScreen_Reset((struct LScreen*)s); + + s->widgets = updates_widgets; + s->maxWidgets = Array_Elems(updates_widgets); + + s->Activated = UpdatesScreen_Activated; + s->Tick = UpdatesScreen_Tick; + s->Deactivated = UpdatesScreen_Deactivated; + s->title = "Update game"; + s->onEscapeWidget = (struct LWidget*)&s->btnBack; + + Launcher_SetScreen((struct LScreen*)s); +} +#endif diff --git a/src/LScreens.h b/src/LScreens.h new file mode 100644 index 0000000..3ac0a19 --- /dev/null +++ b/src/LScreens.h @@ -0,0 +1,52 @@ +#ifndef CC_LSCREENS_H +#define CC_LSCREENS_H +#include "Core.h" +/* +Implements all of the screens/menus in the launcher +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct LWidget; +struct LScreen; +struct Context2D; + +typedef void (*LScreen_Func)(struct LScreen* s); + +#define LScreen_Layout \ + LScreen_Func Activated; /* Called whenever this screen is set as the active screen */ \ + LScreen_Func LoadState; /* Called after the first time this screen is set as the active screen */ \ + LScreen_Func Deactivated; /* Called when the active screen is switched to a different one */ \ + LScreen_Func Layout; /* Positions the widgets on this screen */ \ + LScreen_Func Tick; /* Repeatedly called multiple times every second */ \ + void (*DrawBackground)(struct LScreen* s, struct Context2D* ctx); \ + void (*KeyDown)(struct LScreen* s, int key, cc_bool wasDown); \ + void (*MouseUp)(struct LScreen* s, int idx); \ + void (*MouseWheel)(struct LScreen* s, float delta); \ + void (*ResetArea)(struct Context2D* ctx, int x, int y, int width, int height); \ + struct LWidget* onEnterWidget; /* Default widget to auto-click when Enter is pressed, can be NULL */ \ + struct LWidget* onEscapeWidget; /* Widget to auto-click when Escape is pressed, can be NULL */ \ + struct LWidget* hoveredWidget; /* Widget the mouse is currently hovering over */ \ + struct LWidget* selectedWidget; /* Widget mouse last clicked on */ \ + int numWidgets; /* Number of widgets actually used */ \ + short maxWidgets; /* Maximum number of widgets that can be added to this screen */ \ + cc_bool everShown; /* Whether this screen has ever been shown before */ \ + struct LWidget** widgets; /* Array of pointers to all the widgets in this screen */ \ + const char* title; /* Titlebar text */ + +struct LScreen { LScreen_Layout }; + +void LScreen_SelectWidget(struct LScreen* s, int idx, struct LWidget* w, cc_bool was); +void LScreen_UnselectWidget(struct LScreen* s, int idx, struct LWidget* w); +void LScreen_AddWidget(void* screen, void* widget); + +void ChooseModeScreen_SetActive(cc_bool firstTime); +void ColoursScreen_SetActive(void); +void DirectConnectScreen_SetActive(void); +void MFAScreen_SetActive(void); +void MainScreen_SetActive(void); +void CheckResourcesScreen_SetActive(void); +void FetchResourcesScreen_SetActive(void); +void ServersScreen_SetActive(void); +void SettingsScreen_SetActive(void); +void ThemesScreen_SetActive(void); +void UpdatesScreen_SetActive(void); +#endif diff --git a/src/LWeb.c b/src/LWeb.c new file mode 100644 index 0000000..c0248ff --- /dev/null +++ b/src/LWeb.c @@ -0,0 +1,728 @@ +#include "LWeb.h" +#ifndef CC_BUILD_WEB +#include "String.h" +#include "Launcher.h" +#include "Platform.h" +#include "Stream.h" +#include "Logger.h" +#include "Window.h" +#include "Options.h" +#include "PackedCol.h" +#include "Errors.h" +#include "Utils.h" +#include "Http.h" +#include "LBackend.h" + +/*########################################################################################################################* +*----------------------------------------------------------JSON-----------------------------------------------------------* +*#########################################################################################################################*/ +#define TOKEN_NONE 0 +#define TOKEN_NUM 1 +#define TOKEN_TRUE 2 +#define TOKEN_FALSE 3 +#define TOKEN_NULL 4 +/* Consumes n characters from the JSON stream */ +#define JsonContext_Consume(ctx, n) ctx->cur += n; ctx->left -= n; + +static const cc_string strTrue = String_FromConst("true"); +static const cc_string strFalse = String_FromConst("false"); +static const cc_string strNull = String_FromConst("null"); + +static cc_bool Json_IsWhitespace(char c) { + return c == '\r' || c == '\n' || c == '\t' || c == ' '; +} + +static cc_bool Json_IsNumber(char c) { + return c == '-' || c == '.' || (c >= '0' && c <= '9'); +} + +static cc_bool Json_ConsumeConstant(struct JsonContext* ctx, const cc_string* value) { + int i; + if (value->length > ctx->left) return false; + + for (i = 0; i < value->length; i++) { + if (ctx->cur[i] != value->buffer[i]) return false; + } + + JsonContext_Consume(ctx, value->length); + return true; +} + +static int Json_ConsumeToken(struct JsonContext* ctx) { + char c; + for (; ctx->left && Json_IsWhitespace(*ctx->cur); ) { JsonContext_Consume(ctx, 1); } + if (!ctx->left) return TOKEN_NONE; + + c = *ctx->cur; + if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == '"' || c == ':') { + JsonContext_Consume(ctx, 1); return c; + } + + /* number token forms part of value, don't consume it */ + if (Json_IsNumber(c)) return TOKEN_NUM; + + if (Json_ConsumeConstant(ctx, &strTrue)) return TOKEN_TRUE; + if (Json_ConsumeConstant(ctx, &strFalse)) return TOKEN_FALSE; + if (Json_ConsumeConstant(ctx, &strNull)) return TOKEN_NULL; + + /* invalid token */ + JsonContext_Consume(ctx, 1); + return TOKEN_NONE; +} + +static cc_string Json_ConsumeNumber(struct JsonContext* ctx) { + int len = 0; + for (; ctx->left && Json_IsNumber(*ctx->cur); len++) { JsonContext_Consume(ctx, 1); } + return String_Init(ctx->cur - len, len, len); +} + +static void Json_ConsumeString(struct JsonContext* ctx, cc_string* str) { + int codepoint, h[4]; + char c; + str->length = 0; + + for (; ctx->left;) { + c = *ctx->cur; JsonContext_Consume(ctx, 1); + if (c == '"') return; + if (c != '\\') { String_Append(str, c); continue; } + + /* form of \X */ + if (!ctx->left) break; + c = *ctx->cur; JsonContext_Consume(ctx, 1); + if (c == '/' || c == '\\' || c == '"') { String_Append(str, c); continue; } + if (c == 'n') { String_Append(str, '\n'); continue; } + + /* form of \uYYYY */ + if (c != 'u' || ctx->left < 4) break; + if (!PackedCol_Unhex(ctx->cur, h, 4)) break; + + codepoint = (h[0] << 12) | (h[1] << 8) | (h[2] << 4) | h[3]; + /* don't want control characters in names/software */ + /* TODO: Convert to CP437.. */ + if (codepoint >= 32) String_Append(str, codepoint); + JsonContext_Consume(ctx, 4); + } + + ctx->failed = true; str->length = 0; +} +static cc_string Json_ConsumeValue(int token, struct JsonContext* ctx); + +static void Json_ConsumeObject(struct JsonContext* ctx) { + char keyBuffer[STRING_SIZE]; + cc_string value, oldKey = ctx->curKey; + int token; + ctx->depth++; + ctx->OnNewObject(ctx); + + while (true) { + token = Json_ConsumeToken(ctx); + if (token == ',') continue; + if (token == '}') break; + + if (token != '"') { ctx->failed = true; break; } + String_InitArray(ctx->curKey, keyBuffer); + Json_ConsumeString(ctx, &ctx->curKey); + + token = Json_ConsumeToken(ctx); + if (token != ':') { ctx->failed = true; break; } + + token = Json_ConsumeToken(ctx); + if (token == TOKEN_NONE) { ctx->failed = true; break; } + + value = Json_ConsumeValue(token, ctx); + ctx->OnValue(ctx, &value); + ctx->curKey = oldKey; + } + ctx->depth--; +} + +static void Json_ConsumeArray(struct JsonContext* ctx) { + cc_string value; + int token; + ctx->depth++; + ctx->OnNewArray(ctx); + + while (true) { + token = Json_ConsumeToken(ctx); + if (token == ',') continue; + if (token == ']') break; + + if (token == TOKEN_NONE) { ctx->failed = true; break; } + value = Json_ConsumeValue(token, ctx); + ctx->OnValue(ctx, &value); + } + ctx->depth--; +} + +static cc_string Json_ConsumeValue(int token, struct JsonContext* ctx) { + switch (token) { + case '{': Json_ConsumeObject(ctx); break; + case '[': Json_ConsumeArray(ctx); break; + case '"': Json_ConsumeString(ctx, &ctx->_tmp); return ctx->_tmp; + + case TOKEN_NUM: return Json_ConsumeNumber(ctx); + case TOKEN_TRUE: return strTrue; + case TOKEN_FALSE: return strFalse; + case TOKEN_NULL: break; + } + return String_Empty; +} + +static void Json_NullOnNew(struct JsonContext* ctx) { } +static void Json_NullOnValue(struct JsonContext* ctx, const cc_string* v) { } +void Json_Init(struct JsonContext* ctx, STRING_REF char* str, int len) { + ctx->cur = str; + ctx->left = len; + ctx->failed = false; + ctx->curKey = String_Empty; + ctx->depth = 0; + + ctx->OnNewArray = Json_NullOnNew; + ctx->OnNewObject = Json_NullOnNew; + ctx->OnValue = Json_NullOnValue; + String_InitArray(ctx->_tmp, ctx->_tmpBuffer); +} + +cc_bool Json_Parse(struct JsonContext* ctx) { + int token; + do { + token = Json_ConsumeToken(ctx); + Json_ConsumeValue(token, ctx); + } while (token != TOKEN_NONE); + + return !ctx->failed; +} + +static cc_bool Json_Handle(cc_uint8* data, cc_uint32 len, + JsonOnValue onVal, JsonOnNew newArr, JsonOnNew newObj) { + struct JsonContext ctx; + /* NOTE: classicube.net uses \u JSON for non ASCII, no need to UTF8 convert characters here */ + Json_Init(&ctx, (char*)data, len); + + if (onVal) ctx.OnValue = onVal; + if (newArr) ctx.OnNewArray = newArr; + if (newObj) ctx.OnNewObject = newObj; + return Json_Parse(&ctx); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Web task---------------------------------------------------------* +*#########################################################################################################################*/ +static char servicesBuffer[FILENAME_SIZE]; +static cc_string servicesServer = String_FromArray(servicesBuffer); +static struct StringsBuffer ccCookies; + +static void LWebTask_Reset(struct LWebTask* task) { + task->completed = false; + task->working = true; + task->success = false; +} + +void LWebTask_Tick(struct LWebTask* task, LWebTask_ErrorCallback errorCallback) { + struct HttpRequest item; + + if (task->completed) return; + if (!Http_GetResult(task->reqID, &item)) return; + + task->working = false; + task->completed = true; + task->success = item.success; + + if (item.success) { + task->Handle(item.data, item.size); + } else if (errorCallback) { + errorCallback(&item); + } + HttpRequest_Free(&item); +} + +void LWebTasks_Init(void) { + Options_Get(SOPT_SERVICES, &servicesServer, SERVICES_SERVER); +} + + +/*########################################################################################################################* +*-------------------------------------------------------GetTokenTask------------------------------------------------------* +*#########################################################################################################################*/ +/* +< GET /api/login/ + +> { +> "username": null, +> "authenticated": false, +> "token": "f033ab37c30201f73f142449d037028d", +> "errors": [] +>} +*/ +struct GetTokenTaskData GetTokenTask; + +static void GetTokenTask_OnValue(struct JsonContext* ctx, const cc_string* str) { + if (String_CaselessEqualsConst(&ctx->curKey, "token")) { + String_Copy(&GetTokenTask.token, str); + } else if (String_CaselessEqualsConst(&ctx->curKey, "username")) { + String_Copy(&GetTokenTask.username, str); + } else if (String_CaselessEqualsConst(&ctx->curKey, "errors")) { + if (str->length) GetTokenTask.error = true; + } +} + +static void GetTokenTask_Handle(cc_uint8* data, cc_uint32 len) { + static cc_string err_msg = String_FromConst("Error parsing get login token response JSON"); + + cc_bool success = Json_Handle(data, len, GetTokenTask_OnValue, NULL, NULL); + if (!success) Logger_WarnFunc(&err_msg); +} + +void GetTokenTask_Run(void) { + cc_string url; char urlBuffer[URL_MAX_SIZE]; + static char tokenBuffer[STRING_SIZE]; + static char userBuffer[STRING_SIZE]; + if (GetTokenTask.Base.working) return; + + LWebTask_Reset(&GetTokenTask.Base); + String_InitArray(url, urlBuffer); + String_Format1(&url, "%s/login", &servicesServer); + + String_InitArray(GetTokenTask.token, tokenBuffer); + String_InitArray(GetTokenTask.username, userBuffer); + GetTokenTask.error = false; + + GetTokenTask.Base.Handle = GetTokenTask_Handle; + GetTokenTask.Base.reqID = Http_AsyncGetDataEx(&url, 0, NULL, NULL, &ccCookies); +} + + +/*########################################################################################################################* +*--------------------------------------------------------SignInTask-------------------------------------------------------* +*#########################################################################################################################*/ +/* +< POST /api/login/ +< username=AndrewPH&password=examplePassW0rd&token=f033ab37c30201f73f142449d037028d + +> { +> "username": "AndrewPH", +> "authenticated": true, +> "token": "33e75ff09dd601bbe69f351039152189", +> "errors": [] +> } +*/ +struct SignInTaskData SignInTask; + +static void SignInTask_LogError(const cc_string* str) { + static char errBuffer[128]; + cc_string err; + + if (String_CaselessEqualsConst(str, "username") || String_CaselessEqualsConst(str, "password")) { + SignInTask.error = "&cWrong username or password"; + } else if (String_CaselessEqualsConst(str, "verification")) { + SignInTask.error = "&cAccount verification required"; + } else if (String_CaselessEqualsConst(str, "login_code")) { + SignInTask.error = "&cLogin code required (Check your emails)"; + SignInTask.needMFA = true; + } else if (str->length) { + String_InitArray_NT(err, errBuffer); + String_Format1(&err, "&c%s", str); + + errBuffer[err.length] = '\0'; + SignInTask.error = errBuffer; + } +} + +static void SignInTask_OnValue(struct JsonContext* ctx, const cc_string* str) { + if (String_CaselessEqualsConst(&ctx->curKey, "username")) { + String_Copy(&SignInTask.username, str); + } else if (String_CaselessEqualsConst(&ctx->curKey, "errors")) { + SignInTask_LogError(str); + } +} + +static void SignInTask_Handle(cc_uint8* data, cc_uint32 len) { + static cc_string err_msg = String_FromConst("Error parsing sign in response JSON"); + + cc_bool success = Json_Handle(data, len, SignInTask_OnValue, NULL, NULL); + if (!success) Logger_WarnFunc(&err_msg); +} + +static void SignInTask_Append(cc_string* dst, const char* key, const cc_string* value) { + String_AppendConst(dst, key); + Http_UrlEncodeUtf8(dst, value); +} + +void SignInTask_Run(const cc_string* user, const cc_string* pass, const cc_string* mfaCode) { + cc_string url; char urlBuffer[URL_MAX_SIZE]; + static char userBuffer[STRING_SIZE]; + cc_string args; char argsBuffer[1024]; + if (SignInTask.Base.working) return; + + LWebTask_Reset(&SignInTask.Base); + String_InitArray(url, urlBuffer); + String_Format1(&url, "%s/login", &servicesServer); + + String_InitArray(SignInTask.username, userBuffer); + SignInTask.error = NULL; + SignInTask.needMFA = false; + + String_InitArray(args, argsBuffer); + SignInTask_Append(&args, "username=", user); + SignInTask_Append(&args, "&password=", pass); + SignInTask_Append(&args, "&token=", &GetTokenTask.token); + SignInTask_Append(&args, "&login_code=", mfaCode); + + SignInTask.Base.Handle = SignInTask_Handle; + SignInTask.Base.reqID = Http_AsyncPostData(&url, 0, args.buffer, args.length, &ccCookies); +} + + +/*########################################################################################################################* +*-----------------------------------------------------FetchServerTask-----------------------------------------------------* +*#########################################################################################################################*/ +/* +< GET /api/server/a709fabdf836a2a102c952442bf2dab1 + +> { "servers" : [ +> {"hash": "a709fabdf836a2a102c952442bf2dab1", "maxplayers": 70, "name": "Freebuild server", "players": 5, "software": "MCGalaxy", "uptime": 185447, "country_abbr": "CA"}, +> ]} +*/ +struct FetchServerData FetchServerTask; +static struct ServerInfo* curServer; + +static void ServerInfo_Init(struct ServerInfo* info) { + String_InitArray(info->hash, info->_hashBuffer); + String_InitArray(info->name, info->_nameBuffer); + String_InitArray(info->ip, info->_ipBuffer); + String_InitArray(info->mppass, info->_mppassBuffer); + String_InitArray(info->software, info->_softBuffer); + + info->players = 0; + info->maxPlayers = 0; + info->uptime = 0; + info->featured = false; + info->country[0] = 't'; + info->country[1] = '1'; /* 'T1' for unrecognised country */ + info->_order = -100000; +} + +static void ServerInfo_Parse(struct JsonContext* ctx, const cc_string* val) { + struct ServerInfo* info = curServer; + if (String_CaselessEqualsConst(&ctx->curKey, "hash")) { + String_Copy(&info->hash, val); + } else if (String_CaselessEqualsConst(&ctx->curKey, "name")) { + String_Copy(&info->name, val); + } else if (String_CaselessEqualsConst(&ctx->curKey, "players")) { + Convert_ParseInt(val, &info->players); + } else if (String_CaselessEqualsConst(&ctx->curKey, "maxplayers")) { + Convert_ParseInt(val, &info->maxPlayers); + } else if (String_CaselessEqualsConst(&ctx->curKey, "uptime")) { + Convert_ParseInt(val, &info->uptime); + } else if (String_CaselessEqualsConst(&ctx->curKey, "mppass")) { + String_Copy(&info->mppass, val); + } else if (String_CaselessEqualsConst(&ctx->curKey, "ip")) { + String_Copy(&info->ip, val); + } else if (String_CaselessEqualsConst(&ctx->curKey, "port")) { + Convert_ParseInt(val, &info->port); + } else if (String_CaselessEqualsConst(&ctx->curKey, "software")) { + String_Copy(&info->software, val); + } else if (String_CaselessEqualsConst(&ctx->curKey, "featured")) { + Convert_ParseBool(val, &info->featured); + } else if (String_CaselessEqualsConst(&ctx->curKey, "country_abbr")) { + /* Two letter country codes, see ISO 3166-1 alpha-2 */ + if (val->length < 2) return; + + /* classicube.net only works with lowercase flag urls */ + info->country[0] = val->buffer[0]; Char_MakeLower(info->country[0]); + info->country[1] = val->buffer[1]; Char_MakeLower(info->country[1]); + } +} + +static void FetchServerTask_Handle(cc_uint8* data, cc_uint32 len) { + curServer = &FetchServerTask.server; + Json_Handle(data, len, ServerInfo_Parse, NULL, NULL); +} + +void FetchServerTask_Run(const cc_string* hash) { + cc_string url; char urlBuffer[URL_MAX_SIZE]; + if (FetchServerTask.Base.working) return; + + LWebTask_Reset(&FetchServerTask.Base); + ServerInfo_Init(&FetchServerTask.server); + String_InitArray(url, urlBuffer); + String_Format2(&url, "%s/server/%s", &servicesServer, hash); + + FetchServerTask.Base.Handle = FetchServerTask_Handle; + FetchServerTask.Base.reqID = Http_AsyncGetDataEx(&url, 0, NULL, NULL, &ccCookies); +} + + +/*########################################################################################################################* +*-----------------------------------------------------FetchServersTask----------------------------------------------------* +*#########################################################################################################################*/ +/* +< GET /api/servers/ + +> { "servers" : [ +> {"hash": "a709fabdf836a2a102c952442bf2dab1", "maxplayers": 70, "name": "Freebuild server", "players": 5, "software": "MCGalaxy", "uptime": 185447, "country_abbr": "CA"}, +> {"hash": "23860c5e192cbaa4698408338efd61cc", "maxplayers": 30, "name": "Other server", "players": 0, software: "", "uptime": 54661, "country_abbr": "T1"} +> ]} +*/ +struct FetchServersData FetchServersTask; +static void FetchServersTask_Count(struct JsonContext* ctx) { + /* JSON is expected in this format: */ + /* { "servers" : (depth = 1) */ + /* [ (depth = 2) */ + /* { server1 }, (depth = 3) */ + /* { server2 }, (depth = 3) */ + /* ... */ + if (ctx->depth != 3) return; + FetchServersTask.numServers++; +} + +static void FetchServersTask_Next(struct JsonContext* ctx) { + if (ctx->depth != 3) return; + curServer++; + ServerInfo_Init(curServer); +} + +static void FetchServersTask_Handle(cc_uint8* data, cc_uint32 len) { + static cc_string err_msg = String_FromConst("Error parsing servers list response JSON"); + + int count; + cc_bool success; + Mem_Free(FetchServersTask.servers); + Mem_Free(FetchServersTask.orders); + Session_Save(); + + FetchServersTask.numServers = 0; + FetchServersTask.servers = NULL; + FetchServersTask.orders = NULL; + + FetchServersTask.numServers = 0; + success = Json_Handle(data, len, NULL, NULL, FetchServersTask_Count); + count = FetchServersTask.numServers; + + if (!success) Logger_WarnFunc(&err_msg); + if (count <= 0) return; + FetchServersTask.servers = (struct ServerInfo*)Mem_Alloc(count, sizeof(struct ServerInfo), "servers list"); + FetchServersTask.orders = (cc_uint16*)Mem_Alloc(count, 2, "servers order"); + + curServer = FetchServersTask.servers - 1; + Json_Handle(data, len, ServerInfo_Parse, NULL, FetchServersTask_Next); +} + +void FetchServersTask_Run(void) { + cc_string url; char urlBuffer[URL_MAX_SIZE]; + if (FetchServersTask.Base.working) return; + + LWebTask_Reset(&FetchServersTask.Base); + String_InitArray(url, urlBuffer); + String_Format1(&url, "%s/servers", &servicesServer); + + FetchServersTask.Base.Handle = FetchServersTask_Handle; + FetchServersTask.Base.reqID = Http_AsyncGetDataEx(&url, 0, NULL, NULL, &ccCookies); +} + +void FetchServersTask_ResetOrder(void) { + int i; + for (i = 0; i < FetchServersTask.numServers; i++) { + FetchServersTask.orders[i] = i; + } +} + + +/*########################################################################################################################* +*-----------------------------------------------------CheckUpdateTask-----------------------------------------------------* +*#########################################################################################################################*/ +/* +< GET /builds.json + +> {"latest_ts": 1718187640.9587102, "release_ts": 1693265172.020421, "release_version": "1.3.6"} +*/ +struct CheckUpdateData CheckUpdateTask; +static char relVersionBuffer[16]; + +CC_NOINLINE static cc_uint64 CheckUpdateTask_ParseTime(const cc_string* str) { + cc_string time, fractional; + cc_uint64 secs; + /* timestamp is in form of "seconds.fractional" */ + /* But only need to care about the seconds here */ + String_UNSAFE_Separate(str, '.', &time, &fractional); + + Convert_ParseUInt64(&time, &secs); + return secs; +} + +static void CheckUpdateTask_OnValue(struct JsonContext* ctx, const cc_string* str) { + if (String_CaselessEqualsConst(&ctx->curKey, "release_version")) { + String_Copy(&CheckUpdateTask.latestRelease, str); + } else if (String_CaselessEqualsConst(&ctx->curKey, "latest_ts")) { + CheckUpdateTask.devTimestamp = CheckUpdateTask_ParseTime(str); + } else if (String_CaselessEqualsConst(&ctx->curKey, "release_ts")) { + CheckUpdateTask.relTimestamp = CheckUpdateTask_ParseTime(str); + } +} + +static void CheckUpdateTask_Handle(cc_uint8* data, cc_uint32 len) { + static cc_string err_msg = String_FromConst("Error parsing update check response JSON"); + + cc_bool success = Json_Handle(data, len, CheckUpdateTask_OnValue, NULL, NULL); + if (!success) Logger_WarnFunc(&err_msg); +} + +void CheckUpdateTask_Run(void) { + static const cc_string url = String_FromConst(UPDATES_SERVER "/builds.json"); + if (CheckUpdateTask.Base.working) return; + + LWebTask_Reset(&CheckUpdateTask.Base); + String_InitArray(CheckUpdateTask.latestRelease, relVersionBuffer); + + CheckUpdateTask.Base.Handle = CheckUpdateTask_Handle; + CheckUpdateTask.Base.reqID = Http_AsyncGetData(&url, 0); +} + + +/*########################################################################################################################* +*-----------------------------------------------------FetchUpdateTask-----------------------------------------------------* +*#########################################################################################################################*/ +struct FetchUpdateData FetchUpdateTask; +static void FetchUpdateTask_Handle(cc_uint8* data, cc_uint32 len) { + static const cc_string path = String_FromConst(UPDATE_FILE); + cc_result res; + + res = Stream_WriteAllTo(&path, data, len); + if (res) { Logger_SysWarn(res, "saving update"); return; } + + res = Updater_SetNewBuildTime(FetchUpdateTask.timestamp); + if (res) Logger_SysWarn(res, "setting update time"); + + res = Updater_MarkExecutable(); + if (res) Logger_SysWarn(res, "making update executable"); + +#ifdef CC_BUILD_WIN + Options_SetBool("update-dirty", true); +#endif +} + +void FetchUpdateTask_Run(cc_bool release, int buildIndex) { + cc_string url; char urlBuffer[URL_MAX_SIZE]; + String_InitArray(url, urlBuffer); + + String_Format2(&url, UPDATES_SERVER "/%c/%c", + release ? "release" : "latest", + Updater_Info.builds[buildIndex].path); + + LWebTask_Reset(&FetchUpdateTask.Base); + FetchUpdateTask.timestamp = release ? CheckUpdateTask.relTimestamp : CheckUpdateTask.devTimestamp; + FetchUpdateTask.Base.Handle = FetchUpdateTask_Handle; + FetchUpdateTask.Base.reqID = Http_AsyncGetData(&url, 0); +} + + +/*########################################################################################################################* +*-----------------------------------------------------FetchFlagsTask------------------------------------------------------* +*#########################################################################################################################*/ +struct FetchFlagsData FetchFlagsTask; +static int flagsCount, flagsCapacity; +static struct Flag* flags; + +static void FetchFlagsTask_DownloadNext(void); +static void FetchFlagsTask_Handle(cc_uint8* data, cc_uint32 len) { + struct Flag* flag = &flags[FetchFlagsTask.count]; + LBackend_DecodeFlag(flag, data, len); + + FetchFlagsTask.count++; + FetchFlagsTask_DownloadNext(); +} + +static void FetchFlagsTask_DownloadNext(void) { + cc_string url; char urlBuffer[URL_MAX_SIZE]; + String_InitArray(url, urlBuffer); + + if (FetchFlagsTask.Base.working) return; + if (FetchFlagsTask.count == flagsCount) return; + + LWebTask_Reset(&FetchFlagsTask.Base); + String_Format2(&url, RESOURCE_SERVER "/img/flags/%r%r.png", + &flags[FetchFlagsTask.count].country[0], &flags[FetchFlagsTask.count].country[1]); + + FetchFlagsTask.Base.Handle = FetchFlagsTask_Handle; + FetchFlagsTask.Base.reqID = Http_AsyncGetData(&url, 0); +} + +static void FetchFlagsTask_Ensure(void) { + if (flagsCount < flagsCapacity) return; + flagsCapacity = flagsCount + 10; + + if (flags) { + flags = (struct Flag*)Mem_Realloc(flags, flagsCapacity, sizeof(struct Flag), "flags"); + } else { + flags = (struct Flag*)Mem_Alloc(flagsCapacity, sizeof(struct Flag), "flags"); + } +} + +void FetchFlagsTask_Add(const struct ServerInfo* server) { + int i; + for (i = 0; i < flagsCount; i++) + { + if (flags[i].country[0] != server->country[0]) continue; + if (flags[i].country[1] != server->country[1]) continue; + /* flag is already or will be downloaded */ + return; + } + FetchFlagsTask_Ensure(); + + Bitmap_Init(flags[flagsCount].bmp, 0, 0, NULL); + flags[flagsCount].country[0] = server->country[0]; + flags[flagsCount].country[1] = server->country[1]; + flags[flagsCount].meta = NULL; + + flagsCount++; + FetchFlagsTask_DownloadNext(); +} + +struct Flag* Flags_Get(const struct ServerInfo* server) { + int i; + for (i = 0; i < FetchFlagsTask.count; i++) + { + if (flags[i].country[0] != server->country[0]) continue; + if (flags[i].country[1] != server->country[1]) continue; + return &flags[i]; + } + return NULL; +} + +void Flags_Free(void) { + int i; + for (i = 0; i < FetchFlagsTask.count; i++) { + Mem_Free(flags[i].bmp.scan0); + } + + flagsCount = 0; + FetchFlagsTask.count = 0; +} + + +/*########################################################################################################################* +*------------------------------------------------------Session cache------------------------------------------------------* +*#########################################################################################################################*/ +static cc_string sessionKey = String_FromConst("session"); +static cc_bool loadedSession; + +void Session_Load(void) { + cc_string session; char buffer[3072]; + if (loadedSession) return; + loadedSession = true; + /* Increase from max 512 to 2048 per entry */ + StringsBuffer_SetLengthBits(&ccCookies, 11); + + String_InitArray(session, buffer); + Options_GetSecure(LOPT_SESSION, &session); + if (!session.length) return; + EntryList_Set(&ccCookies, &sessionKey, &session, '='); +} + +void Session_Save(void) { + cc_string session = EntryList_UNSAFE_Get(&ccCookies, &sessionKey, '='); + if (!session.length) return; + Options_SetSecure(LOPT_SESSION, &session); +} +#endif diff --git a/src/LWeb.h b/src/LWeb.h new file mode 100644 index 0000000..b9edd9a --- /dev/null +++ b/src/LWeb.h @@ -0,0 +1,136 @@ +#ifndef CC_LWEB_H +#define CC_LWEB_H +#include "Bitmap.h" +#include "Constants.h" +/* Implements asynchronous web tasks for the launcher. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct HttpRequest; +struct JsonContext; +typedef void (*JsonOnValue)(struct JsonContext* ctx, const cc_string* v); +typedef void (*JsonOnNew)(struct JsonContext* ctx); + +/* State for parsing JSON text */ +struct JsonContext { + char* cur; /* Pointer to current character in JSON stream being inspected. */ + int left; /* Number of characters left to be inspected. */ + cc_bool failed; /* Whether there was an error parsing the JSON. */ + cc_string curKey; /* Key/Name of current member */ + int depth; /* Object/Array depth (e.g. { { { is depth 3 */ + + JsonOnNew OnNewArray; /* Invoked when start of an array is read. */ + JsonOnNew OnNewObject; /* Invoked when start of an object is read. */ + JsonOnValue OnValue; /* Invoked on each member value in an object/array. */ + cc_string _tmp; /* temp value used for reading string values */ + char _tmpBuffer[STRING_SIZE]; +}; +/* Initialises state of JSON parser. */ +void Json_Init(struct JsonContext* ctx, STRING_REF char* str, int len); +/* Parses the JSON text, invoking callbacks when value/array/objects are read. */ +/* NOTE: DO NOT persist the value argument in OnValue. */ +cc_bool Json_Parse(struct JsonContext* ctx); + +/* Represents all known details about a server. */ +struct ServerInfo { + cc_string hash, name, ip, mppass, software; + int players, maxPlayers, port, uptime; + cc_bool featured; + char country[2]; + int _order; /* (internal) order in servers table after filtering */ + char _hashBuffer[32], _nameBuffer[STRING_SIZE]; + char _ipBuffer[16], _mppassBuffer[STRING_SIZE]; + char _softBuffer[STRING_SIZE]; +}; + +/* Represents a country flag */ +struct Flag { + struct Bitmap bmp; + char country[2]; /* ISO 3166-1 alpha-2 */ + void* meta; /* Backend specific meta */ +}; + +struct LWebTask { + cc_bool completed; /* Whether the task has finished executing. */ + cc_bool working; /* Whether the task is currently in progress, or is scheduled to be. */ + cc_bool success; /* Whether the task completed successfully. */ + + int reqID; /* Unique request identifier for this web task. */ + /* Called when task successfully downloaded/uploaded data. */ + void (*Handle)(cc_uint8* data, cc_uint32 len); +}; +typedef void (*LWebTask_ErrorCallback)(struct HttpRequest* req); + +void LWebTask_Tick(struct LWebTask* task, LWebTask_ErrorCallback errorCallback); +void LWebTasks_Init(void); + + +extern struct GetTokenTaskData { + struct LWebTask Base; + cc_string token; /* Random CSRF token for logging in. */ + cc_string username; /* Username if session is already logged in. */ + cc_bool error; /* Whether a signin error occurred */ +} GetTokenTask; +void GetTokenTask_Run(void); + +extern struct SignInTaskData { + struct LWebTask Base; + cc_string username; /* Username to sign in as. Changed to case correct username. */ + const char* error; /* If sign in fails, the reason why. */ + cc_bool needMFA; /* need login code for multifactor authentication */ +} SignInTask; +void SignInTask_Run(const cc_string* user, const cc_string* pass, const cc_string* mfaCode); + + +extern struct FetchServerData { + struct LWebTask Base; + struct ServerInfo server; /* Details about the given server on success. */ +} FetchServerTask; +void FetchServerTask_Run(const cc_string* hash); + + +extern struct FetchServersData { + struct LWebTask Base; + struct ServerInfo* servers; /* List of all public servers on server list. */ + cc_uint16* orders; /* Order of each server (after sorting) */ + int numServers; /* Number of public servers. */ +} FetchServersTask; +void FetchServersTask_Run(void); +void FetchServersTask_ResetOrder(void); +#define Servers_Get(i) (&FetchServersTask.servers[FetchServersTask.orders[i]]) + + +extern struct CheckUpdateData { + struct LWebTask Base; + /* Unix timestamp latest commit/dev build and release were at. */ + cc_uint64 devTimestamp, relTimestamp; + /* Version of latest release. */ + cc_string latestRelease; +} CheckUpdateTask; /* TODO: Work out the JSON for this.. */ +void CheckUpdateTask_Run(void); + + +extern struct FetchUpdateData { + struct LWebTask Base; + /* Unix timestamp downloaded build was originally built at. */ + cc_uint64 timestamp; +} FetchUpdateTask; +void FetchUpdateTask_Run(cc_bool release, int buildIndex); + + +extern struct FetchFlagsData { + struct LWebTask Base; + /* Number of flags downloaded. */ + int count; +} FetchFlagsTask; + +/* Asynchronously downloads the flag associated with the given server's country. */ +void FetchFlagsTask_Add(const struct ServerInfo* server); +/* Gets the country flag associated with the given server's country. */ +struct Flag* Flags_Get(const struct ServerInfo* server); +/* Frees all flag bitmaps. */ +void Flags_Free(void); + +void Session_Load(void); +void Session_Save(void); +#endif diff --git a/src/LWidgets.c b/src/LWidgets.c new file mode 100644 index 0000000..7a7da60 --- /dev/null +++ b/src/LWidgets.c @@ -0,0 +1,793 @@ +#include "LWidgets.h" +#ifndef CC_BUILD_WEB +#include "String.h" +#include "Gui.h" +#include "Drawer2D.h" +#include "Launcher.h" +#include "ExtMath.h" +#include "Window.h" +#include "Funcs.h" +#include "LWeb.h" +#include "Platform.h" +#include "LScreens.h" +#include "Input.h" +#include "Utils.h" +#include "LBackend.h" + +static int flagXOffset, flagYOffset; +static int oneX, twoX, fourX; +static int oneY, twoY, fourY; + +void LWidget_CalcOffsets(void) { + oneX = Display_ScaleX(1); + oneY = Display_ScaleY(1); + + if (oneX < 1) { oneX = 1; } + if (oneY < 1) { oneY = 1; } + + twoX = oneX * 2; fourX = oneX * 4; + twoY = oneY * 2; fourY = oneY * 4; + + flagXOffset = Display_ScaleX(2); + flagYOffset = Display_ScaleY(6); +} + +static void LWidget_DrawInsetBorder(struct Context2D* ctx, BitmapCol color, int insetX, int insetY, + int x, int y, int width, int height) { + Context2D_Clear(ctx, color, + x + insetX, y, + width - 2 * insetX, insetY); + Context2D_Clear(ctx, color, + x + insetX, y + height - insetY, + width - 2 * insetX, insetY); + Context2D_Clear(ctx, color, + x, y + insetY, + insetX, height - 2 * insetY); + Context2D_Clear(ctx, color, + x + width - insetX, y + insetY, + insetX, height - 2 * insetY); +} + +void LWidget_DrawBorder(struct Context2D* ctx, BitmapCol color, int borderX, int borderY, + int x, int y, int width, int height) { + Context2D_Clear(ctx, color, + x, y, + width, borderY); + Context2D_Clear(ctx, color, + x, y + height - borderY, + width, borderY); + Context2D_Clear(ctx, color, + x, y, + borderX, height); + Context2D_Clear(ctx, color, + x + width - borderX, y, + borderX, height); +} + + +/*########################################################################################################################* +*------------------------------------------------------ButtonWidget-------------------------------------------------------* +*#########################################################################################################################*/ +static void LButton_DrawBase(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active) { + BitmapCol color = active ? Launcher_Theme.ButtonForeActiveColor + : Launcher_Theme.ButtonForeColor; + + if (Launcher_Theme.ClassicBackground) { + Gradient_Noise(ctx, color, 8, + x + oneX, y + oneY, + width - twoX, height - twoY); + } else { + Gradient_Vertical(ctx, BitmapColor_Offset(color, 8,8,8), BitmapColor_Offset(color, -8,-8,-8), + x + oneX, y + oneY, + width - twoX, height - twoY); + } +} + +static void LButton_DrawBorder(struct Context2D* ctx, int x, int y, int width, int height) { + BitmapCol backColor = Launcher_Theme.ButtonBorderColor; +#ifdef CC_BUILD_IOS + LWidget_DrawBorder(ctx, backColor, oneX, oneY, x, y, width, height); +#else + LWidget_DrawInsetBorder(ctx, backColor, oneX, oneY, x, y, width, height); +#endif +} + +static void LButton_DrawHighlight(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active) { + BitmapCol activeColor = BitmapColor_RGB(189, 198, 255); + BitmapCol color = Launcher_Theme.ButtonHighlightColor; + + if (Launcher_Theme.ClassicBackground) { + if (active) color = activeColor; + + Context2D_Clear(ctx, color, + x + twoX, y + oneY, + width - fourX, oneY); + Context2D_Clear(ctx, color, + x + oneX, y + twoY, + oneX, height - fourY); + } else if (!active) { + Context2D_Clear(ctx, color, + x + twoX, y + oneY, + width - fourX, oneY); + } +} + +void LButton_DrawBackground(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active) { + LButton_DrawBase( ctx, x, y, width, height, active); + LButton_DrawBorder( ctx, x, y, width, height); + LButton_DrawHighlight(ctx, x, y, width, height, active); +} + +static void LButton_Draw(void* widget) { + struct LButton* w = (struct LButton*)widget; + LBackend_ButtonDraw(w); +} + +static void LButton_Hover(void* w, int idx, cc_bool wasOver) { + /* only need to redraw when changing from unhovered to active */ + if (!wasOver) LBackend_MarkDirty(w); +} + +static void LButton_Unhover(void* w) { LBackend_MarkDirty(w); } +static void LButton_OnSelect(void* w, int idx, cc_bool wasSelected) { LBackend_MarkDirty(w); } +static void LButton_OnUnselect(void* w, int idx) { LBackend_MarkDirty(w); } + +static const struct LWidgetVTABLE lbutton_VTABLE = { + LButton_Draw, NULL, + NULL, NULL, /* Key */ + LButton_Hover, LButton_Unhover, /* Hover */ + LButton_OnSelect, LButton_OnUnselect /* Select */ +}; +void LButton_Add(void* screen, struct LButton* w, int width, int height, const char* text, + LWidgetFunc onClick, const struct LLayout* layouts) { + w->VTABLE = &lbutton_VTABLE; + w->type = LWIDGET_BUTTON; + w->OnClick = onClick; + w->layouts = layouts; + w->autoSelectable = true; + + LBackend_ButtonInit(w, width, height); + LButton_SetConst(w, text); + LScreen_AddWidget(screen, w); +} + +void LButton_SetConst(struct LButton* w, const char* text) { + w->text = String_FromReadonly(text); + LBackend_ButtonUpdate(w); +} + + +/*########################################################################################################################* +*-----------------------------------------------------CheckboxWidget------------------------------------------------------* +*#########################################################################################################################*/ +static void LCheckbox_Draw(void* widget) { + struct LCheckbox* w = (struct LCheckbox*)widget; + LBackend_CheckboxDraw(w); +} + +static const struct LWidgetVTABLE lcheckbox_VTABLE = { + LCheckbox_Draw, NULL, + NULL, NULL, /* Key */ + NULL, NULL, /* Hover */ + NULL, NULL /* Select */ +}; +void LCheckbox_Add(void* screen, struct LCheckbox* w, const char* text, + LCheckboxChanged onChanged, const struct LLayout* layouts) { + w->VTABLE = &lcheckbox_VTABLE; + w->type = LWIDGET_CHECKBOX; + w->layouts = layouts; + w->autoSelectable = true; + w->ValueChanged = onChanged; + + w->text = String_FromReadonly(text); + LBackend_CheckboxInit(w); + LScreen_AddWidget(screen, w); +} + +void LCheckbox_Set(struct LCheckbox* w, cc_bool value) { + w->value = value; + LBackend_CheckboxUpdate(w); +} + + +/*########################################################################################################################* +*------------------------------------------------------InputWidget--------------------------------------------------------* +*#########################################################################################################################*/ +void LInput_UNSAFE_GetText(struct LInput* w, cc_string* text) { + int i; + if (w->inputType != KEYBOARD_TYPE_PASSWORD) { *text = w->text; return; } + + for (i = 0; i < w->text.length; i++) { + String_Append(text, '*'); + } +} + +static void LInput_Draw(void* widget) { + struct LInput* w = (struct LInput*)widget; + LBackend_InputDraw(w); +} + +static void LInput_TickCaret(void* widget) { + struct LInput* w = (struct LInput*)widget; + LBackend_InputTick(w); +} + +static void LInput_Select(void* widget, int idx, cc_bool wasSelected) { + struct LInput* w = (struct LInput*)widget; + LBackend_InputSelect(w, idx, wasSelected); +} + +static void LInput_Unselect(void* widget, int idx) { + struct LInput* w = (struct LInput*)widget; + LBackend_InputUnselect(w); +} + +static void LInput_AdvanceCaretPos(struct LInput* w, cc_bool forwards) { + if (forwards && w->caretPos == -1) return; + if (!forwards && w->caretPos == 0) return; + if (w->caretPos == -1 && !forwards) /* caret after text */ + w->caretPos = w->text.length; + + w->caretPos += (forwards ? 1 : -1); + if (w->caretPos < 0 || w->caretPos >= w->text.length) w->caretPos = -1; + LBackend_InputUpdate(w); +} + +static void LInput_CopyFromClipboard(struct LInput* w) { + cc_string text; char textBuffer[2048]; + String_InitArray(text, textBuffer); + + Clipboard_GetText(&text); + String_UNSAFE_TrimStart(&text); + String_UNSAFE_TrimEnd(&text); + + if (w->ClipboardFilter) w->ClipboardFilter(&text); + LInput_AppendString(w, &text); +} + +/* If caret position is now beyond end of text, resets to -1 */ +static CC_INLINE void LInput_ClampCaret(struct LInput* w) { + if (w->caretPos >= w->text.length) w->caretPos = -1; +} + +/* Removes the character preceding the caret in the currently entered text */ +static void LInput_Backspace(struct LInput* w) { + if (!w->text.length || w->caretPos == 0) return; + + if (w->caretPos == -1) { + String_DeleteAt(&w->text, w->text.length - 1); + } else { + String_DeleteAt(&w->text, w->caretPos - 1); + w->caretPos--; + if (w->caretPos == -1) w->caretPos = 0; + } + + if (w->TextChanged) w->TextChanged(w); + LInput_ClampCaret(w); + LBackend_InputUpdate(w); +} + +/* Removes the character at the caret in the currently entered text */ +static void LInput_Delete(struct LInput* w) { + if (!w->text.length || w->caretPos == -1) return; + + String_DeleteAt(&w->text, w->caretPos); + if (w->caretPos == -1) w->caretPos = 0; + + if (w->TextChanged) w->TextChanged(w); + LInput_ClampCaret(w); + LBackend_InputUpdate(w); +} + +static cc_bool LInput_KeyDown(void* widget, int key, cc_bool was) { + struct LInput* w = (struct LInput*)widget; + if (key == CCKEY_BACKSPACE) { + LInput_Backspace(w); + } else if (key == CCKEY_DELETE) { + LInput_Delete(w); + } else if (key == INPUT_CLIPBOARD_COPY) { + if (w->text.length) Clipboard_SetText(&w->text); + } else if (key == INPUT_CLIPBOARD_PASTE) { + LInput_CopyFromClipboard(w); + } else if (Input_IsEscapeButton(key)) { + if (w->text.length) LInput_SetString(w, &String_Empty); + } else if (Input_IsLeftButton(key)) { + LInput_AdvanceCaretPos(w, false); + } else if (Input_IsRightButton(key)) { + LInput_AdvanceCaretPos(w, true); + } else { return false; } + + return true; +} + +static cc_bool LInput_CanAppend(struct LInput* w, char c) { + switch (w->inputType) { + case KEYBOARD_TYPE_PASSWORD: + return true; /* keyboard accepts all characters */ + case KEYBOARD_TYPE_INTEGER: + return c >= '0' && c <= '9'; + } + return c >= ' ' && c <= '~' && c != '&'; +} + +/* Appends a character to the currently entered text */ +static CC_NOINLINE cc_bool LInput_Append(struct LInput* w, char c) { + if (LInput_CanAppend(w, c) && w->text.length < w->text.capacity) { + if (w->caretPos == -1) { + String_Append(&w->text, c); + } else { + String_InsertAt(&w->text, w->caretPos, c); + w->caretPos++; + } + return true; + } + return false; +} + +static void LInput_KeyChar(void* widget, char c) { + struct LInput* w = (struct LInput*)widget; + cc_bool appended = LInput_Append(w, c); + + if (appended && w->TextChanged) w->TextChanged(w); + if (appended) LBackend_InputUpdate(w); +} + +static void LInput_TextChanged(void* widget, const cc_string* str) { + struct LInput* w = (struct LInput*)widget; + LInput_SetText(w, str); + if (w->TextChanged) w->TextChanged(w); +} + +static const struct LWidgetVTABLE linput_VTABLE = { + LInput_Draw, LInput_TickCaret, + LInput_KeyDown, LInput_KeyChar, /* Key */ + NULL, NULL, /* Hover */ + /* TODO: Don't redraw whole thing, just the outer border */ + LInput_Select, LInput_Unselect, /* Select */ + NULL, LInput_TextChanged /* TextChanged */ +}; +void LInput_Add(void* screen, struct LInput* w, int width, const char* hintText, + const struct LLayout* layouts) { + w->VTABLE = &linput_VTABLE; + w->type = LWIDGET_INPUT; + w->autoSelectable = true; + w->opaque = true; + w->layouts = layouts; + + /* Preserve existing input across Add calls */ + if (!w->text.buffer) { + String_InitArray(w->text, w->_textBuffer); + } + + w->hintText = hintText; + w->caretPos = -1; + LBackend_InputInit(w, width); + LScreen_AddWidget(screen, w); +} + +void LInput_SetText(struct LInput* w, const cc_string* text) { + String_Copy(&w->text, text); + LInput_ClampCaret(w); + LBackend_InputUpdate(w); +} + +void LInput_ClearText(struct LInput* w) { + w->text.length = 0; + w->caretPos = -1; + LBackend_InputUpdate(w); +} + +void LInput_AppendString(struct LInput* w, const cc_string* str) { + int i, appended = 0; + for (i = 0; i < str->length; i++) { + if (LInput_Append(w, str->buffer[i])) appended++; + } + + if (appended && w->TextChanged) w->TextChanged(w); + if (appended) LBackend_InputUpdate(w); +} + +void LInput_SetString(struct LInput* w, const cc_string* str) { + LInput_SetText(w, str); + if (w->TextChanged) w->TextChanged(w); +} + + +/*########################################################################################################################* +*------------------------------------------------------LabelWidget--------------------------------------------------------* +*#########################################################################################################################*/ +static void LLabel_Draw(void* widget) { + struct LLabel* w = (struct LLabel*)widget; + LBackend_LabelDraw(w); +} + +static const struct LWidgetVTABLE llabel_VTABLE = { + LLabel_Draw, NULL, + NULL, NULL, /* Key */ + NULL, NULL, /* Hover */ + NULL, NULL /* Select */ +}; +void LLabel_Add(void* screen, struct LLabel* w, const char* text, + const struct LLayout* layouts) { + w->VTABLE = &llabel_VTABLE; + w->type = LWIDGET_LABEL; + w->layouts = layouts; + + String_InitArray(w->text, w->_textBuffer); + LBackend_LabelInit(w); + LLabel_SetConst(w, text); + LScreen_AddWidget(screen, w); +} + +void LLabel_SetText(struct LLabel* w, const cc_string* text) { + String_Copy(&w->text, text); + LBackend_LabelUpdate(w); + LBackend_LayoutWidget((struct LWidget*)w); +} + +void LLabel_SetConst(struct LLabel* w, const char* text) { + cc_string str = String_FromReadonly(text); + LLabel_SetText(w, &str); +} + + +/*########################################################################################################################* +*-------------------------------------------------------LineWidget--------------------------------------------------------* +*#########################################################################################################################*/ +static void LLine_Draw(void* widget) { + struct LLine* w = (struct LLine*)widget; + LBackend_LineDraw(w); +} + +static const struct LWidgetVTABLE lline_VTABLE = { + LLine_Draw, NULL, + NULL, NULL, /* Key */ + NULL, NULL, /* Hover */ + NULL, NULL /* Select */ +}; +void LLine_Add(void* screen, struct LLine* w, int width, + const struct LLayout* layouts) { + w->VTABLE = &lline_VTABLE; + w->type = LWIDGET_LINE; + w->layouts = layouts; + + LBackend_LineInit(w, width); + LScreen_AddWidget(screen, w); +} + +#define CLASSIC_LINE_COLOR BitmapColor_RGB(128, 128, 128) +BitmapCol LLine_GetColor(void) { + return Launcher_Theme.ClassicBackground ? CLASSIC_LINE_COLOR : Launcher_Theme.ButtonBorderColor; +} + + +/*########################################################################################################################* +*------------------------------------------------------SliderWidget-------------------------------------------------------* +*#########################################################################################################################*/ +static void LSlider_Draw(void* widget) { + struct LSlider* w = (struct LSlider*)widget; + LBackend_SliderDraw(w); +} + +static const struct LWidgetVTABLE lslider_VTABLE = { + LSlider_Draw, NULL, + NULL, NULL, /* Key */ + NULL, NULL, /* Hover */ + NULL, NULL /* Select */ +}; +void LSlider_Add(void* screen, struct LSlider* w, int width, int height, BitmapCol color, + const struct LLayout* layouts) { + w->VTABLE = &lslider_VTABLE; + w->type = LWIDGET_SLIDER; + w->color = color; + w->opaque = true; + w->layouts = layouts; + + LBackend_SliderInit(w, width, height); + LScreen_AddWidget(screen, w); +} + +void LSlider_SetProgress(struct LSlider* w, int progress) { + if (progress == w->value) return; + w->value = progress; + LBackend_SliderUpdate(w); +} + + +/*########################################################################################################################* +*------------------------------------------------------TableWidget--------------------------------------------------------* +*#########################################################################################################################*/ +static void FlagColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) { + struct Flag* flag = Flags_Get(row); + if (!flag) return; + Context2D_DrawPixels(ctx, cell->x + flagXOffset, cell->y + flagYOffset, &flag->bmp); +} + +static void NameColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) { + args->text = row->name; +} +static int NameColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) { + return String_Compare(&b->name, &a->name); +} + +static void PlayersColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) { + String_Format2(&args->text, "%i/%i", &row->players, &row->maxPlayers); +} +static int PlayersColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) { + return b->players - a->players; +} + +static void UptimeColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) { + LTable_FormatUptime(&args->text, row->uptime); +} +static int UptimeColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) { + return b->uptime - a->uptime; +} + +static void SoftwareColumn_Draw(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx) { + /* last column, so adjust to fit size of table */ + int leftover = cell->table->width - cell->x; + cell->width = max(cell->width, leftover); + args->text = row->software; +} +static int SoftwareColumn_Sort(const struct ServerInfo* a, const struct ServerInfo* b) { + return String_Compare(&b->software, &a->software); +} + +static struct LTableColumn tableColumns[] = { + { "", 15, FlagColumn_Draw, NULL, false, false, false }, + { "Name", 328, NameColumn_Draw, NameColumn_Sort, true, false, true }, + { "Players", 73, PlayersColumn_Draw, PlayersColumn_Sort, true, true, true }, + { "Uptime", 73, UptimeColumn_Draw, UptimeColumn_Sort, true, true, true }, + { "Software", 143, SoftwareColumn_Draw, SoftwareColumn_Sort, false, true, true } +}; + + +void LTable_FormatUptime(cc_string* dst, int uptime) { + char unit = 's'; + + if (uptime >= SECS_PER_DAY * 7) { + uptime /= SECS_PER_DAY; unit = 'd'; + } else if (uptime >= SECS_PER_HOUR) { + uptime /= SECS_PER_HOUR; unit = 'h'; + } else if (uptime >= SECS_PER_MIN) { + uptime /= SECS_PER_MIN; unit = 'm'; + } + String_Format2(dst, "%i%r", &uptime, &unit); +} + +void LTable_GetScrollbarCoords(struct LTable* w, int* y, int* height) { + float scale; + if (!w->rowsCount) { *y = 0; *height = 0; return; } + + scale = w->height / (float)w->rowsCount; + *y = Math_Ceil(w->topRow * scale); + *height = Math_Ceil(w->visibleRows * scale); + *height = min(*y + *height, w->height) - *y; +} + +void LTable_ClampTopRow(struct LTable* w) { + if (w->topRow > w->rowsCount - w->visibleRows) { + w->topRow = w->rowsCount - w->visibleRows; + } + if (w->topRow < 0) w->topRow = 0; +} + +int LTable_GetSelectedIndex(struct LTable* w) { + struct ServerInfo* entry; + int row; + + for (row = 0; row < w->rowsCount; row++) { + entry = LTable_Get(row); + if (String_CaselessEquals(w->selectedHash, &entry->hash)) return row; + } + return -1; +} + +void LTable_SetSelectedTo(struct LTable* w, int index) { + if (!w->rowsCount) return; + if (index >= w->rowsCount) index = w->rowsCount - 1; + if (index < 0) index = 0; + + String_Copy(w->selectedHash, <able_Get(index)->hash); + LTable_ShowSelected(w); + w->OnSelectedChanged(); +} + +void LTable_RowClick(struct LTable* w, int row) { + cc_uint64 now; + LTable_SetSelectedTo(w, row); + now = Stopwatch_Measure(); + + /* double click on row to join */ + if (Stopwatch_ElapsedMS(w->_lastClick, now) < 1000 && row == w->_lastRow) { + Launcher_ConnectToServer(<able_Get(row)->hash); + } + + w->_lastRow = LTable_GetSelectedIndex(w); + w->_lastClick = now; +} + +cc_bool LTable_HandlesKey(int key) { + return Input_IsUpButton(key) || key == CCKEY_PAGEUP || + Input_IsDownButton(key) || key == CCKEY_PAGEDOWN; +} + +static cc_bool LTable_KeyDown(void* widget, int key, cc_bool was) { + struct LTable* w = (struct LTable*)widget; + int index = LTable_GetSelectedIndex(w); + + if (Input_IsUpButton(key)) { + index--; + } else if (Input_IsDownButton(key)) { + index++; + } else if (key == CCKEY_PAGEUP) { + index -= w->visibleRows; + } else if (key == CCKEY_PAGEDOWN) { + index += w->visibleRows; + } else { return false; } + + w->_lastRow = -1; + LTable_SetSelectedTo(w, index); + return true; +} + +static void LTable_MouseDown(void* widget, int idx, cc_bool wasOver) { + struct LTable* w = (struct LTable*)widget; + LBackend_TableMouseDown(w, idx); +} + +static void LTable_MouseMove(void* widget, int idx, cc_bool wasOver) { + struct LTable* w = (struct LTable*)widget; + LBackend_TableMouseMove(w, idx); +} + +static void LTable_MouseUp(void* widget, int idx) { + struct LTable* w = (struct LTable*)widget; + LBackend_TableMouseUp(w, idx); +} + +static void LTable_MouseWheel(void* widget, float delta) { + struct LTable* w = (struct LTable*)widget; + w->topRow -= Utils_AccumulateWheelDelta(&w->_wheelAcc, delta); + LTable_ClampTopRow(w); + LBackend_MarkDirty(w); + w->_lastRow = -1; +} + +static void LTable_Draw(void* widget) { + struct LTable* w = (struct LTable*)widget; + LBackend_TableDraw(w); +} + +static const struct LWidgetVTABLE ltable_VTABLE = { + LTable_Draw, NULL, + LTable_KeyDown, NULL, /* Key */ + LTable_MouseMove, NULL, /* Hover */ + LTable_MouseDown, LTable_MouseUp, /* Select */ + LTable_MouseWheel, /* Wheel */ +}; +void LTable_Add(void* screen, struct LTable* w, + const struct LLayout* layouts) { + int i; + w->VTABLE = <able_VTABLE; + w->type = LWIDGET_TABLE; + w->columns = tableColumns; + w->numColumns = Array_Elems(tableColumns); + w->sortingCol = -1; + w->opaque = true; + w->layouts = layouts; + + for (i = 0; i < w->numColumns; i++) { + w->columns[i].width = Display_ScaleX(w->columns[i].width); + } + LBackend_TableInit(w); + LScreen_AddWidget(screen, w); +} + +void LTable_Reset(struct LTable* w) { + LBackend_TableMouseUp(w, 0); + LBackend_TableReposition(w); + + w->topRow = 0; + w->rowsCount = 0; + w->_wheelAcc = 0.0f; + w->sortingCol = -1; +} + +static int ShouldShowServer(struct LTable* w, struct ServerInfo* server) { + return String_CaselessContains(&server->name, w->filter) + && (Launcher_ShowEmptyServers || server->players > 0); +} + +void LTable_ApplyFilter(struct LTable* w) { + int i, j, count; + + count = FetchServersTask.numServers; + for (i = 0, j = 0; i < count; i++) { + if (ShouldShowServer(w, Servers_Get(i))) { + FetchServersTask.servers[j++]._order = FetchServersTask.orders[i]; + } + } + + w->rowsCount = j; + for (; j < count; j++) { + FetchServersTask.servers[j]._order = -100000; + } + + w->_lastRow = -1; + LTable_ClampTopRow(w); + LBackend_TableUpdate(w); +} + +static int sortingCol; +static int LTable_SortOrder(const struct ServerInfo* a, const struct ServerInfo* b) { + int order; + if (sortingCol >= 0) { + order = tableColumns[sortingCol].SortOrder(a, b); + return tableColumns[sortingCol].invertSort ? -order : order; + } + + /* Default sort order. (most active server, then by highest uptime) */ + if (a->players != b->players) return a->players - b->players; + return a->uptime - b->uptime; +} + +static void LTable_QuickSort(int left, int right) { + cc_uint16* keys = FetchServersTask.orders; cc_uint16 key; + + while (left < right) { + int i = left, j = right; + struct ServerInfo* mid = Servers_Get((i + j) >> 1); + + /* partition the list */ + while (i <= j) { + while (LTable_SortOrder(mid, Servers_Get(i)) < 0) i++; + while (LTable_SortOrder(mid, Servers_Get(j)) > 0) j--; + QuickSort_Swap_Maybe(); + } + /* recurse into the smaller subset */ + QuickSort_Recurse(LTable_QuickSort) + } +} + +void LTable_Sort(struct LTable* w) { + sortingCol = w->sortingCol; + FetchServersTask_ResetOrder(); + + if (FetchServersTask.numServers) + LTable_QuickSort(0, FetchServersTask.numServers - 1); + + LTable_ApplyFilter(w); + LTable_ShowSelected(w); +} + +void LTable_ShowSelected(struct LTable* w) { + int i = LTable_GetSelectedIndex(w); + if (i == -1) return; + + if (i >= w->topRow + w->visibleRows) { + w->topRow = i - (w->visibleRows - 1); + } + if (i < w->topRow) w->topRow = i; + LTable_ClampTopRow(w); +} + +BitmapCol LTable_RowColor(int row, cc_bool selected, cc_bool featured) { + BitmapCol featSelColor = BitmapColor_RGB( 50, 53, 0); + BitmapCol featuredColor = BitmapColor_RGB(101, 107, 0); + BitmapCol selectedColor = BitmapColor_RGB( 40, 40, 40); + + if (featured) { + return selected ? featSelColor : featuredColor; + } else if (selected) { + return selectedColor; + } + + if (!Launcher_Theme.ClassicBackground) { + return BitmapColor_RGB(20, 20, 10); + } else { + return (row & 1) == 0 ? Launcher_Theme.BackgroundColor : 0; + } +} +#endif diff --git a/src/LWidgets.h b/src/LWidgets.h new file mode 100644 index 0000000..61b33c2 --- /dev/null +++ b/src/LWidgets.h @@ -0,0 +1,251 @@ +#ifndef CC_LWIDGETS_H +#define CC_LWIDGETS_H +#include "Bitmap.h" +#include "Constants.h" +/* Describes and manages individual 2D GUI elements in the launcher. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct FontDesc; +struct Context2D; +enum LWIDGET_TYPE { + LWIDGET_BUTTON, LWIDGET_CHECKBOX, LWIDGET_INPUT, + LWIDGET_LABEL, LWIDGET_LINE, LWIDGET_SLIDER, LWIDGET_TABLE +}; + +#define LLAYOUT_EXTRA 0x0100 +#define LLAYOUT_WIDTH 0x0200 +#define LLAYOUT_HEIGHT 0x0300 +struct LLayout { short type, offset; }; + +struct LWidgetVTABLE { + /* Called to draw contents of this widget */ + void (*Draw)(void* widget); + /* Called repeatedly to update this widget when selected. */ + void (*Tick)(void* widget); + /* Called when key is pressed and this widget is selected. */ + /* Returns whether the key press was intercepted */ + cc_bool (*KeyDown)(void* widget, int key, cc_bool wasDown); + /* Called when key is pressed and this widget is selected. */ + void (*KeyPress)(void* widget, char c); + /* Called when mouse hovers/moves over this widget. */ + void (*MouseMove)(void* widget, int idx, cc_bool wasOver); + /* Called when mouse moves away from this widget. */ + void (*MouseLeft)(void* widget); + /* Called when mouse clicks on this widget. */ + /* NOTE: This function is just for general widget behaviour. */ + /* OnClick callback is for per-widget instance behaviour. */ + void (*OnSelect)(void* widget, int idx, cc_bool wasSelected); + /* Called when mouse clicks on another widget. */ + void (*OnUnselect)(void* widget, int idx); + /* Called when mouse wheel is scrolled and this widget is selected. */ + void (*MouseWheel)(void* widget, float delta); + /* Called when on-screen keyboard text changed. */ + void (*TextChanged)(void* elem, const cc_string* str); +}; + + +#define LWidget_Layout \ + const struct LWidgetVTABLE* VTABLE; /* General widget functions */ \ + int x, y, width, height; /* Top left corner and dimensions of this widget */ \ + cc_bool hovered; /* Whether this widget is currently being moused over */ \ + cc_bool selected; /* Whether this widget is last widget to be clicked on */ \ + cc_bool autoSelectable; /* Whether this widget can get auto selected (e.g. pressing tab) */ \ + cc_bool dirty; /* Whether this widget needs to be redrawn */ \ + cc_bool opaque; /* Whether this widget completely obscures background behind it */ \ + cc_uint8 type; /* Type of this widget */ \ + cc_bool skipsEnter; /* Whether clicking this widget DOESN'T trigger OnEnterWidget */ \ + LWidgetFunc OnClick; /* Called when widget is clicked */ \ + LWidgetFunc OnHover; /* Called when widget is hovered over */ \ + LWidgetFunc OnUnhover; /* Called when widget is no longer hovered over */ \ + Rect2D last; /* Widget's last drawn area */ \ + void* meta; /* Backend specific data */ \ + const struct LLayout* layouts; + +typedef void (*LWidgetFunc)(void* widget); +/* Represents an individual 2D gui component in the launcher. */ +struct LWidget { LWidget_Layout }; +void LWidget_CalcOffsets(void); + +struct LButton { + LWidget_Layout + cc_string text; + int _textWidth, _textHeight; +}; +CC_NOINLINE void LButton_Add(void* screen, struct LButton* w, int width, int height, const char* text, + LWidgetFunc onClick, const struct LLayout* layouts); +CC_NOINLINE void LButton_SetConst(struct LButton* w, const char* text); +CC_NOINLINE void LButton_DrawBackground(struct Context2D* ctx, int x, int y, int width, int height, cc_bool active); + +struct LCheckbox; +typedef void (*LCheckboxChanged)(struct LCheckbox* cb); +struct LCheckbox { + LWidget_Layout + cc_bool value; + cc_string text; + LCheckboxChanged ValueChanged; +}; +CC_NOINLINE void LCheckbox_Add(void* screen, struct LCheckbox* w, const char* text, + LCheckboxChanged onChanged, const struct LLayout* layouts); +CC_NOINLINE void LCheckbox_Set(struct LCheckbox* w, cc_bool value); + +struct LInput; +struct LInput { + LWidget_Layout + int minWidth; + /* Text displayed when the user has not entered anything in the text field. */ + const char* hintText; + /* The type of this input (see KEYBOARD_TYPE_ enum in Window.h) */ + /* If type is KEYBOARD_TYPE_PASSWORD, all characters are drawn as *. */ + cc_uint8 inputType; + /* Whether caret is currently visible */ + cc_bool caretShow; + /* Filter applied to text received from the clipboard. Can be NULL. */ + void (*ClipboardFilter)(cc_string* str); + /* Callback invoked when the text is changed. Can be NULL. */ + void (*TextChanged)(struct LInput* w); + /* Specifies the position that characters are inserted/deleted from. */ + /* NOTE: -1 to insert/delete characters at end of the text. */ + int caretPos; + cc_string text; + int _textHeight; + char _textBuffer[STRING_SIZE * 2]; +}; +CC_NOINLINE void LInput_Add(void* screen, struct LInput* w, int width, const char* hintText, + const struct LLayout* layouts); +CC_NOINLINE void LInput_UNSAFE_GetText(struct LInput* w, cc_string* text); +CC_NOINLINE void LInput_SetText(struct LInput* w, const cc_string* text); +CC_NOINLINE void LInput_ClearText(struct LInput* w); + +/* Appends a string to the currently entered text */ +CC_NOINLINE void LInput_AppendString(struct LInput* w, const cc_string* str); +/* Sets the currently entered text to the given string */ +CC_NOINLINE void LInput_SetString(struct LInput* w, const cc_string* str); +#define LINPUT_HEIGHT 30 + +/* Represents non-interactable text. */ +struct LLabel { + LWidget_Layout + cc_bool small; /* whether to use 12pt instead of 14pt font size */ + cc_string text; + char _textBuffer[STRING_SIZE]; +}; +CC_NOINLINE void LLabel_Add(void* screen, struct LLabel* w, const char* text, + const struct LLayout* layouts); +CC_NOINLINE void LLabel_SetText(struct LLabel* w, const cc_string* text); +CC_NOINLINE void LLabel_SetConst(struct LLabel* w, const char* text); + +/* Represents a coloured translucent line separator. */ +struct LLine { + LWidget_Layout + int _width; +}; +CC_NOINLINE void LLine_Add(void* screen, struct LLine* w, int width, + const struct LLayout* layouts); +CC_NOINLINE BitmapCol LLine_GetColor(void); +#define LLINE_HEIGHT 2 + +/* Represents a slider bar that may or may not be modifiable by the user. */ +struct LSlider { + LWidget_Layout + int value, _width, _height; + BitmapCol color; +}; +CC_NOINLINE void LSlider_Add(void* screen, struct LSlider* w, int width, int height, BitmapCol color, + const struct LLayout* layouts); +CC_NOINLINE void LSlider_SetProgress(struct LSlider* w, int progress); + +struct ServerInfo; +struct DrawTextArgs; +struct LTableCell; + +struct LTableColumn { + /* Name of this column. */ + const char* name; + /* Width of this column in pixels. */ + int width; + /* Draws the value of this column for the given row. */ + /* If args.Text is changed to something, that text gets drawn afterwards. */ + /* Most of the time that's all you need to do. */ + void (*DrawRow)(struct ServerInfo* row, struct DrawTextArgs* args, struct LTableCell* cell, struct Context2D* ctx); + /* Returns sort order of two rows, based on value of this column in both rows. */ + int (*SortOrder)(const struct ServerInfo* a, const struct ServerInfo* b); + /* Whether a vertical gridline (and padding) appears after this. */ + cc_bool hasGridline; + /* Whether user can resize this column. */ + cc_bool draggable; + /* Whether user can sort this column. */ + cc_bool sortable; + /* Whether to invert the order of row sorting. */ + cc_bool invertSort; +}; + +/* Represents a table of server entries. */ +struct LTable { + LWidget_Layout + /* Columns of the table. */ + struct LTableColumn* columns; + /* Number of columns in the table. */ + int numColumns; + /* Y start and end of rows and height of each row. */ + int rowsBegY, rowsEndY, rowHeight; + /* Y height of headers. */ + int hdrHeight; + /* Maximum number of rows visible. */ + int visibleRows; + /* Total number of rows in the table (after filter is applied). */ + int rowsCount; + /* Index of top row currently visible. */ + int topRow; + + /* Hash of the currently selected server. */ + cc_string* selectedHash; + /* Filter for which server names to show. */ + cc_string* filter; + /* Callback when selected has has changed. */ + void (*OnSelectedChanged)(void); + + /* Index of table column currently being dragged. */ + int draggingColumn; + int dragXStart; /* X coordinate column drag started at */ + cc_bool draggingScrollbar; /* Is scrollbar is currently being dragged */ + int dragYOffset; /* Offset of mouse for scrollbar dragging */ + + float _wheelAcc; /* mouse wheel accumulator */ + int _lastRow; /* last clicked row (for doubleclick join) */ + cc_uint64 _lastClick; /* timestamp of last mouse click on a row */ + int sortingCol; +}; + +struct LTableCell { struct LTable* table; int x, y, width; }; +/* Gets the current ith row */ +#define LTable_Get(row) (&FetchServersTask.servers[FetchServersTask.servers[row]._order]) + +/* Initialises a table. */ +/* NOTE: Must also call LTable_Reset to make a table actually useful. */ +void LTable_Add(void* screen, struct LTable* table, + const struct LLayout* layouts); +/* Resets state of a table (reset sorter, filter, etc) */ +void LTable_Reset(struct LTable* table); +/* Whether this table would handle the given key being pressed. */ +/* e.g. used so pressing up/down works even when another widget is selected */ +cc_bool LTable_HandlesKey(int key); +/* Filters rows to only show those containing 'w->Filter' in the name. */ +void LTable_ApplyFilter(struct LTable* table); +/* Sorts the rows in the table by current Sorter function of table */ +void LTable_Sort(struct LTable* table); +/* If selected row is not visible, adjusts top row so it does show. */ +void LTable_ShowSelected(struct LTable* table); + +void LTable_FormatUptime(cc_string* dst, int uptime); +/* Works out top and height of the scrollbar */ +void LTable_GetScrollbarCoords(struct LTable* w, int* y, int* height); +/* Ensures top/first visible row index lies within table */ +void LTable_ClampTopRow(struct LTable* w); +/* Returns index of selected row in currently visible rows */ +int LTable_GetSelectedIndex(struct LTable* w); +/* Sets selected row to given row, scrolling table if needed */ +void LTable_SetSelectedTo(struct LTable* w, int index); +void LTable_RowClick(struct LTable* w, int row); +/* Works out the background color of the given row */ +BitmapCol LTable_RowColor(int row, cc_bool selected, cc_bool featured); +#endif diff --git a/src/Launcher.c b/src/Launcher.c new file mode 100644 index 0000000..ebeea9d --- /dev/null +++ b/src/Launcher.c @@ -0,0 +1,583 @@ +#include "Launcher.h" +#ifndef CC_BUILD_WEB +#include "String.h" +#include "LScreens.h" +#include "LWidgets.h" +#include "LWeb.h" +#include "Resources.h" +#include "Drawer2D.h" +#include "Game.h" +#include "Deflate.h" +#include "Stream.h" +#include "Utils.h" +#include "Input.h" +#include "Window.h" +#include "Event.h" +#include "Http.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Logger.h" +#include "Options.h" +#include "LBackend.h" +#include "PackedCol.h" +#include "SystemFonts.h" +#include "TexturePack.h" +#include "Gui.h" + +struct LScreen* Launcher_Active; +cc_bool Launcher_ShouldExit, Launcher_ShouldUpdate; +static char hashBuffer[STRING_SIZE], userBuffer[STRING_SIZE]; +cc_string Launcher_AutoHash = String_FromArray(hashBuffer); +cc_string Launcher_Username = String_FromArray(userBuffer); +cc_bool Launcher_ShowEmptyServers; + +static cc_bool useBitmappedFont, hasBitmappedFont; +static struct Bitmap dirtBmp, stoneBmp; +#define TILESIZE 48 + +static void CloseActiveScreen(void) { + OnscreenKeyboard_Close(); + if (!Launcher_Active) return; + + Launcher_Active->Deactivated(Launcher_Active); + LBackend_CloseScreen(Launcher_Active); + Launcher_Active = NULL; +} + +void Launcher_SetScreen(struct LScreen* screen) { + CloseActiveScreen(); + Launcher_Active = screen; + + screen->Activated(screen); + screen->Layout(screen); + + if (!screen->everShown) screen->LoadState(screen); + screen->everShown = true; + + LBackend_SetScreen(screen); + LBackend_Redraw(); +} + +void Launcher_DisplayHttpError(struct HttpRequest* req, const char* action, cc_string* dst) { + cc_result res = req->result; + int status = req->statusCode; + + if (res) { + /* Non HTTP error - this is not good */ + Http_LogError(action, req); + String_Format2(dst, "&cError %e when %c", &res, action); + } else if (status != 200) { + String_Format2(dst, "&c%i error when %c", &status, action); + } else { + String_Format1(dst, "&cEmpty response when %c", action); + } +} + + +/*########################################################################################################################* +*--------------------------------------------------------Starter/Updater--------------------------------------------------* +*#########################################################################################################################*/ +static cc_uint64 lastJoin; +cc_bool Launcher_StartGame(const cc_string* user, const cc_string* mppass, const cc_string* ip, const cc_string* port, const cc_string* server) { + cc_string args[4]; int numArgs; + cc_uint64 now; + cc_result res; + + now = Stopwatch_Measure(); + if (Stopwatch_ElapsedMS(lastJoin, now) < 1000) return false; + lastJoin = now; + + /* Save resume info */ + if (server->length) { + Options_PauseSaving(); + Options_Set(ROPT_SERVER, server); + Options_Set(ROPT_USER, user); + Options_Set(ROPT_IP, ip); + Options_Set(ROPT_PORT, port); + Options_SetSecure(ROPT_MPPASS, mppass); + Options_ResumeSaving(); + } + /* Save options BEFORE starting new game process */ + /* Otherwise can get 'file already in use' errors on startup */ + Options_SaveIfChanged(); + + args[0] = *user; + numArgs = 1; + if (mppass->length) { + args[1] = *mppass; + args[2] = *ip; + args[3] = *port; + numArgs = 4; + } + + res = Process_StartGame2(args, numArgs); + if (res) { Logger_SysWarn(res, "starting game"); return false; } + + Launcher_ShouldExit = Platform_SingleProcess || Options_GetBool(LOPT_AUTO_CLOSE, false); + + return true; +} + +CC_NOINLINE static void StartFromInfo(struct ServerInfo* info) { + cc_string port; char portBuffer[STRING_INT_CHARS]; + String_InitArray(port, portBuffer); + + String_AppendInt(&port, info->port); + Launcher_StartGame(&Launcher_Username, &info->mppass, &info->ip, &port, &info->name); +} + +static void ConnectToServerError(struct HttpRequest* req) { + cc_string logMsg = String_Init(NULL, 0, 0); + Launcher_DisplayHttpError(req, "fetching server info", &logMsg); +} + +cc_bool Launcher_ConnectToServer(const cc_string* hash) { + struct ServerInfo* info; + int i; + if (!hash->length) return false; + + for (i = 0; i < FetchServersTask.numServers; i++) { + info = &FetchServersTask.servers[i]; + if (!String_Equals(hash, &info->hash)) continue; + + StartFromInfo(info); + return true; + } + + /* Fallback to private server handling */ + /* TODO: Rewrite to be async */ + FetchServerTask_Run(hash); + + while (!FetchServerTask.Base.completed) { + LWebTask_Tick(&FetchServerTask.Base, ConnectToServerError); + Thread_Sleep(10); + } + + if (FetchServerTask.server.hash.length) { + StartFromInfo(&FetchServerTask.server); + return true; + } else if (FetchServerTask.Base.success) { + Window_ShowDialog("Failed to connect", "No server has that hash"); + } + return false; +} + + +/*########################################################################################################################* +*---------------------------------------------------------Event handler---------------------------------------------------* +*#########################################################################################################################*/ +static void OnResize(void* obj) { + LBackend_FreeFramebuffer(); + LBackend_InitFramebuffer(); + + if (Launcher_Active) Launcher_Active->Layout(Launcher_Active); + LBackend_Redraw(); +} + +static cc_bool IsShutdown(int key) { + if (key == CCKEY_F4 && Input_IsAltPressed()) return true; + + /* On macOS, Cmd+Q should also end the process */ +#ifdef CC_BUILD_DARWIN + return key == 'Q' && Input_IsWinPressed(); +#else + return false; +#endif +} + +static void OnInputDown(void* obj, int key, cc_bool was) { + if (Window_Main.SoftKeyboardFocus) return; + + if (IsShutdown(key)) Launcher_ShouldExit = true; + Launcher_Active->KeyDown(Launcher_Active, key, was); +} + +static void OnMouseWheel(void* obj, float delta) { + Launcher_Active->MouseWheel(Launcher_Active, delta); +} + + +/*########################################################################################################################* +*-----------------------------------------------------------Main body-----------------------------------------------------* +*#########################################################################################################################*/ +static void Launcher_Init(void) { + Event_Register_(&WindowEvents.Resized, NULL, OnResize); + Event_Register_(&WindowEvents.StateChanged, NULL, OnResize); + + Event_Register_(&InputEvents.Down, NULL, OnInputDown); + Event_Register_(&InputEvents.Wheel, NULL, OnMouseWheel); + + Utils_EnsureDirectory("texpacks"); + Utils_EnsureDirectory("audio"); +} + +static void Launcher_Free(void) { + Event_UnregisterAll(); + LBackend_Free(); + Flags_Free(); + hasBitmappedFont = false; + + CloseActiveScreen(); + LBackend_FreeFramebuffer(); +} + +void Launcher_Run(void) { + static const cc_string title = String_FromConst(GAME_APP_TITLE); + Window_Create2D(640, 400); +#ifdef CC_BUILD_MOBILE + Window_LockLandscapeOrientation(Options_GetBool(OPT_LANDSCAPE_MODE, false)); +#endif + Window_SetTitle(&title); + Window_Show(); + LWidget_CalcOffsets(); + +#ifdef CC_BUILD_WIN + /* clean leftover exe from updating */ + if (Options_GetBool("update-dirty", false) && Updater_Clean()) { + Options_Set("update-dirty", NULL); + } +#endif + Drawer2D_Component.Init(); + SystemFonts_Component.Init(); + Drawer2D.BitmappedText = false; + Drawer2D.BlackTextShadows = true; + + LBackend_Init(); + LBackend_InitFramebuffer(); + Launcher_ShowEmptyServers = Options_GetBool(LOPT_SHOW_EMPTY, true); + Options_Get(LOPT_USERNAME, &Launcher_Username, ""); + + LWebTasks_Init(); + Session_Load(); + Launcher_LoadTheme(); + Launcher_Init(); + + GameVersion_Load(); + Launcher_TryLoadTexturePack(); + + Http_Component.Init(); + CheckUpdateTask_Run(); + +#ifdef CC_BUILD_RESOURCES + Resources_CheckExistence(); + + if (Resources_MissingCount) { + CheckResourcesScreen_SetActive(); + } else { + MainScreen_SetActive(); + } +#else + MainScreen_SetActive(); +#endif + + for (;;) { + Window_ProcessEvents(10 / 1000.0f); + Window_ProcessGamepads(10 / 1000.0f); + Gamepad_Tick(10 / 1000.0f); + if (!Window_Main.Exists || Launcher_ShouldExit) break; + + Launcher_Active->Tick(Launcher_Active); + LBackend_Tick(); + Thread_Sleep(10); + } + + Options_SaveIfChanged(); + Launcher_Free(); + Launcher_ShouldExit = false; + +#ifdef CC_BUILD_MOBILE + /* Reset components */ + Platform_LogConst("undoing components"); + Drawer2D_Component.Free(); + Http_Component.Free(); +#endif + + if (Launcher_ShouldUpdate) { + const char* action; + cc_result res = Updater_Start(&action); + if (res) Logger_SysWarn(res, action); + } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Colours/Skin----------------------------------------------------* +*#########################################################################################################################*/ +struct LauncherTheme Launcher_Theme; +const struct LauncherTheme Launcher_ModernTheme = { + false, + BitmapColor_RGB(153, 127, 172), /* background */ + BitmapColor_RGB( 97, 81, 110), /* button border */ + BitmapColor_RGB(189, 168, 206), /* active button */ + BitmapColor_RGB(141, 114, 165), /* button foreground */ + BitmapColor_RGB(162, 131, 186), /* button highlight */ +}; +const struct LauncherTheme Launcher_ClassicTheme = { + true, + BitmapColor_RGB( 41, 41, 41), /* background */ + BitmapColor_RGB( 0, 0, 0), /* button border */ + BitmapColor_RGB(126, 136, 191), /* active button */ + BitmapColor_RGB(111, 111, 111), /* button foreground */ + BitmapColor_RGB(168, 168, 168), /* button highlight */ +}; +const struct LauncherTheme Launcher_NordicTheme = { + false, + BitmapColor_RGB( 46, 52, 64), /* background */ + BitmapColor_RGB( 59, 66, 82), /* button border */ + BitmapColor_RGB( 66, 74, 90), /* active button */ + BitmapColor_RGB( 59, 66, 82), /* button foreground */ + BitmapColor_RGB( 76, 86, 106), /* button highlight */ +}; + +CC_NOINLINE static void ParseColor(const char* key, BitmapCol* color) { + cc_uint8 rgb[3]; + if (!Options_GetColor(key, rgb)) return; + + *color = BitmapColor_RGB(rgb[0], rgb[1], rgb[2]); +} + +void Launcher_LoadTheme(void) { + if (Options_GetBool(OPT_CLASSIC_MODE, false)) { + Launcher_Theme = Launcher_ClassicTheme; + return; + } + Launcher_Theme = Launcher_NordicTheme; + Launcher_Theme.ClassicBackground = Options_GetBool("nostalgia-classicbg", false); + + ParseColor("launcher-back-col", &Launcher_Theme.BackgroundColor); + ParseColor("launcher-btn-border-col", &Launcher_Theme.ButtonBorderColor); + ParseColor("launcher-btn-fore-active-col", &Launcher_Theme.ButtonForeActiveColor); + ParseColor("launcher-btn-fore-inactive-col", &Launcher_Theme.ButtonForeColor); + ParseColor("launcher-btn-highlight-inactive-col", &Launcher_Theme.ButtonHighlightColor); +} + +CC_NOINLINE static void SaveColor(const char* key, BitmapCol color) { + cc_string value; char valueBuffer[6]; + + String_InitArray(value, valueBuffer); + String_AppendHex(&value, BitmapCol_R(color)); + String_AppendHex(&value, BitmapCol_G(color)); + String_AppendHex(&value, BitmapCol_B(color)); + Options_Set(key, &value); +} + +void Launcher_SaveTheme(void) { + Options_PauseSaving(); + SaveColor("launcher-back-col", Launcher_Theme.BackgroundColor); + SaveColor("launcher-btn-border-col", Launcher_Theme.ButtonBorderColor); + SaveColor("launcher-btn-fore-active-col", Launcher_Theme.ButtonForeActiveColor); + SaveColor("launcher-btn-fore-inactive-col", Launcher_Theme.ButtonForeColor); + SaveColor("launcher-btn-highlight-inactive-col", Launcher_Theme.ButtonHighlightColor); + Options_SetBool("nostalgia-classicbg", Launcher_Theme.ClassicBackground); + Options_ResumeSaving(); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Texture pack----------------------------------------------------* +*#########################################################################################################################*/ +/* Tints the given area, linearly interpolating from a to b */ +/* Note that this only tints RGB, A is not tinted */ +static void TintBitmap(struct Bitmap* bmp, cc_uint8 tintA, cc_uint8 tintB, int width, int height) { + BitmapCol* row; + cc_uint8 tint; + int xx, yy; + + for (yy = 0; yy < height; yy++) { + row = Bitmap_GetRow(bmp, yy); + tint = (cc_uint8)Math_Lerp(tintA, tintB, (float)yy / height); + + for (xx = 0; xx < width; xx++) { + /* TODO: Not shift when multiplying */ + row[xx] = BitmapColor_RGB( + BitmapCol_R(row[xx]) * tint / 255, + BitmapCol_G(row[xx]) * tint / 255, + BitmapCol_B(row[xx]) * tint / 255); + } + } +} + +static void ExtractTerrainTiles(struct Bitmap* bmp) { + int tileSize = bmp->width / 16; + Bitmap_Allocate(&dirtBmp, TILESIZE, TILESIZE); + Bitmap_Allocate(&stoneBmp, TILESIZE, TILESIZE); + + /* Precompute the scaled background */ + Bitmap_Scale(&dirtBmp, bmp, 2 * tileSize, 0, tileSize, tileSize); + Bitmap_Scale(&stoneBmp, bmp, 1 * tileSize, 0, tileSize, tileSize); + + TintBitmap(&dirtBmp, 128, 64, TILESIZE, TILESIZE); + TintBitmap(&stoneBmp, 96, 96, TILESIZE, TILESIZE); +} + +static cc_bool Launcher_SelectZipEntry(const cc_string* path) { + return + String_CaselessEqualsConst(path, "default.png") || + String_CaselessEqualsConst(path, "terrain.png"); +} + +static cc_result Launcher_ProcessZipEntry(const cc_string* path, struct Stream* data, struct ZipEntry* source) { + struct Bitmap bmp; + cc_result res; + + + if (String_CaselessEqualsConst(path, "default.png")) { + if (hasBitmappedFont) return 0; + hasBitmappedFont = false; + res = Png_Decode(&bmp, data); + + if (res) { + Logger_SysWarn(res, "decoding default.png"); return res; + } else if (Font_SetBitmapAtlas(&bmp)) { + useBitmappedFont = !Options_GetBool(OPT_USE_CHAT_FONT, false); + hasBitmappedFont = true; + } else { + Mem_Free(bmp.scan0); + } + } else if (String_CaselessEqualsConst(path, "terrain.png")) { + if (dirtBmp.scan0 != NULL) return 0; + res = Png_Decode(&bmp, data); + + if (res) { + Logger_SysWarn(res, "decoding terrain.png"); return res; + } else { + ExtractTerrainTiles(&bmp); + } + } + return 0; +} + +static cc_result ExtractTexturePack(const cc_string* path) { + struct Stream stream; + cc_result res; + + res = Stream_OpenFile(&stream, path); + if (res == ReturnCode_FileNotFound) return res; + if (res) { Logger_SysWarn(res, "opening texture pack"); return res; } + + res = Zip_Extract(&stream, + Launcher_SelectZipEntry, Launcher_ProcessZipEntry); + if (res) { Logger_SysWarn(res, "extracting texture pack"); } + /* No point logging error for closing readonly file */ + (void)stream.Close(&stream); + return res; +} + +void Launcher_TryLoadTexturePack(void) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + cc_string texPack; + + /* TODO: Not duplicate TexturePack functionality */ + if (Options_UNSAFE_Get(OPT_DEFAULT_TEX_PACK, &texPack)) { + String_InitArray(path, pathBuffer); + String_Format1(&path, "texpacks/%s", &texPack); + (void)ExtractTexturePack(&path); + } + + /* user selected texture pack is missing some required .png files */ + if (!hasBitmappedFont || dirtBmp.scan0 == NULL) + TexturePack_ExtractDefault(ExtractTexturePack); + + LBackend_UpdateTitleFont(); +} + + +/*########################################################################################################################* +*----------------------------------------------------------Background-----------------------------------------------------* +*#########################################################################################################################*/ +/* Fills the given area using pixels from the source bitmap, by repeatedly tiling the bitmap */ +CC_NOINLINE static void ClearTile(int x, int y, int width, int height, + struct Context2D* ctx, struct Bitmap* src) { + struct Bitmap* dst = (struct Bitmap*)ctx; + BitmapCol* dstRow; + BitmapCol* srcRow; + int xx, yy; + if (!Drawer2D_Clamp(ctx, &x, &y, &width, &height)) return; + + for (yy = 0; yy < height; yy++) { + srcRow = Bitmap_GetRow(src, (y + yy) % TILESIZE); + dstRow = Bitmap_GetRow(dst, y + yy) + x; + + for (xx = 0; xx < width; xx++) { + dstRow[xx] = srcRow[(x + xx) % TILESIZE]; + } + } +} + +void Launcher_DrawBackground(struct Context2D* ctx, int x, int y, int width, int height) { + if (Launcher_Theme.ClassicBackground && dirtBmp.scan0) { + ClearTile(x, y, width, height, ctx, &stoneBmp); + } else { + Gradient_Noise(ctx, Launcher_Theme.BackgroundColor, 6, x, y, width, height); + } +} + +void Launcher_DrawBackgroundAll(struct Context2D* ctx) { + if (Launcher_Theme.ClassicBackground && dirtBmp.scan0) { + ClearTile(0, 0, ctx->width, TILESIZE, ctx, &dirtBmp); + ClearTile(0, TILESIZE, ctx->width, ctx->height - TILESIZE, ctx, &stoneBmp); + } else { + Launcher_DrawBackground(ctx, 0, 0, ctx->width, ctx->height); + } +} + +cc_bool Launcher_BitmappedText(void) { + return (useBitmappedFont || Launcher_Theme.ClassicBackground) && hasBitmappedFont; +} + +static void DrawTitleText(struct FontDesc* font, const char* text, struct Context2D* ctx, + cc_uint8 horAnchor, cc_uint8 verAnchor) { + cc_string title = String_FromReadonly(text); + struct DrawTextArgs args; + int x, y; + + DrawTextArgs_Make(&args, &title, font, false); + x = Gui_CalcPos(horAnchor, 0, Drawer2D_TextWidth(&args), ctx->width); + y = Gui_CalcPos(verAnchor, 0, Drawer2D_TextHeight(&args), ctx->height); + + Drawer2D.Colors['f'] = BITMAPCOLOR_BLACK; + Context2D_DrawText(ctx, &args, x + Display_ScaleX(4), y + Display_ScaleY(4)); + Drawer2D.Colors['f'] = BITMAPCOLOR_WHITE; + Context2D_DrawText(ctx, &args, x, y); +} + +#ifdef CC_BUILD_DUALSCREEN +extern cc_bool launcherTop; + +void Launcher_DrawTitle(struct FontDesc* font, const char* text, struct Context2D* ctx) { + /* Put title on top screen */ + struct Context2D topCtx; + struct Bitmap bmp; + int width = Window_Alt.Width; + int height = Window_Alt.Height; + launcherTop = true; + + ctx = &topCtx; + Window_AllocFramebuffer(&bmp, width, height); + Context2D_Wrap(ctx, &bmp); + topCtx.width = width; + topCtx.height = height; + + Launcher_DrawBackgroundAll(ctx); + DrawTitleText(font, text, ctx, ANCHOR_CENTRE, ANCHOR_CENTRE); + Rect2D rect = { 0, 0, bmp.width, bmp.height }; + Window_DrawFramebuffer(rect, &bmp); + + Window_FreeFramebuffer(&bmp); + launcherTop = false; +} +#else +void Launcher_DrawTitle(struct FontDesc* font, const char* text, struct Context2D* ctx) { + /* Skip dragging logo when very small window to save space */ + if (Window_Main.Height < 240) return; + + DrawTitleText(font, text, ctx, ANCHOR_CENTRE, ANCHOR_MIN); +} +#endif + +void Launcher_MakeTitleFont(struct FontDesc* font) { + Drawer2D.BitmappedText = Launcher_BitmappedText(); + Font_Make(font, 32, FONT_FLAGS_NONE); + Drawer2D.BitmappedText = false; +} +#endif diff --git a/src/Launcher.h b/src/Launcher.h new file mode 100644 index 0000000..84a9ccf --- /dev/null +++ b/src/Launcher.h @@ -0,0 +1,81 @@ +#ifndef CC_LAUNCHER_H +#define CC_LAUNCHER_H +#include "Bitmap.h" +/* Implements the launcher part of the game. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct LScreen; +struct FontDesc; +struct Context2D; +struct HttpRequest; + +/* The screen/menu currently being shown */ +extern struct LScreen* Launcher_Active; + +/* Whether at the next tick, the launcher window should proceed to stop displaying frames and subsequently exit */ +extern cc_bool Launcher_ShouldExit; +/* Whether game should be updated on exit */ +extern cc_bool Launcher_ShouldUpdate; +/* (optional) Hash of the server the game should automatically try to connect to after signing in */ +extern cc_string Launcher_AutoHash; +/* The username of currently active user */ +extern cc_string Launcher_Username; +/* Whether to show empty servers in the server list */ +extern cc_bool Launcher_ShowEmptyServers; + +struct LauncherTheme { + /* Whether to use stone tile background like minecraft.net */ + cc_bool ClassicBackground; + /* Base colour of pixels before any widgets are drawn */ + BitmapCol BackgroundColor; + /* Colour of pixels on the 4 line borders around buttons */ + BitmapCol ButtonBorderColor; + /* Colour of button when user has mouse over it */ + BitmapCol ButtonForeActiveColor; + /* Colour of button when user does not have mouse over it */ + BitmapCol ButtonForeColor; + /* Colour of line at top of buttons to give them a less flat look*/ + BitmapCol ButtonHighlightColor; +}; +/* Currently active theme */ +extern struct LauncherTheme Launcher_Theme; +/* Modern / enhanced theme */ +extern const struct LauncherTheme Launcher_ModernTheme; +/* Minecraft Classic theme */ +extern const struct LauncherTheme Launcher_ClassicTheme; +/* Custom Nordic style theme */ +extern const struct LauncherTheme Launcher_NordicTheme; + +/* Loads theme from options. */ +void Launcher_LoadTheme(void); +/* Saves the theme to options. */ +/* NOTE: Does not save options file itself. */ +void Launcher_SaveTheme(void); + +/* Whether logo should be drawn using bitmapped text */ +cc_bool Launcher_BitmappedText(void); +/* Draws title styled text using the given font */ +void Launcher_DrawTitle(struct FontDesc* font, const char* text, struct Context2D* ctx); +/* Allocates a font appropriate for drawing title text */ +void Launcher_MakeTitleFont(struct FontDesc* font); + +/* Attempts to load font and terrain from texture pack. */ +void Launcher_TryLoadTexturePack(void); +/* Fills the given region of the given bitmap with the default background */ +void Launcher_DrawBackground(struct Context2D* ctx, int x, int y, int width, int height); +/* Fills the entire contents of the given bitmap with the default background */ +/* NOTE: Also draws titlebar at top, if current screen permits it */ +void Launcher_DrawBackgroundAll(struct Context2D* ctx); + +/* Sets currently active screen/menu, freeing old one. */ +void Launcher_SetScreen(struct LScreen* screen); +/* Attempts to start the game by connecting to the given server. */ +cc_bool Launcher_ConnectToServer(const cc_string* hash); +/* Launcher main loop. */ +void Launcher_Run(void); +/* Starts the game from the given arguments. */ +cc_bool Launcher_StartGame(const cc_string* user, const cc_string* mppass, const cc_string* ip, const cc_string* port, const cc_string* server); +/* Prints information about a http error to dst. (for status widget) */ +/* If req->result is non-zero, also displays a dialog box on-screen. */ +void Launcher_DisplayHttpError(struct HttpRequest* req, const char* action, cc_string* dst); +#endif diff --git a/src/Lighting.c b/src/Lighting.c new file mode 100644 index 0000000..3800d80 --- /dev/null +++ b/src/Lighting.c @@ -0,0 +1,475 @@ +#include "Lighting.h" +#include "Block.h" +#include "Funcs.h" +#include "MapRenderer.h" +#include "Platform.h" +#include "World.h" +#include "Logger.h" +#include "Event.h" +#include "Game.h" +#include "String.h" +#include "Chat.h" +#include "ExtMath.h" +#include "Options.h" +#include "Builder.h" + +const char* const LightingMode_Names[LIGHTING_MODE_COUNT] = { "Classic", "Fancy" }; + +cc_uint8 Lighting_Mode; +cc_bool Lighting_ModeLockedByServer; +cc_bool Lighting_ModeSetByServer; +cc_uint8 Lighting_ModeUserCached; +struct _Lighting Lighting; +#define Lighting_Pack(x, z) ((x) + World.Width * (z)) + +void Lighting_SetMode(cc_uint8 mode, cc_bool fromServer) { + cc_uint8 oldMode = Lighting_Mode; + Lighting_Mode = mode; + + Event_RaiseLightingMode(&WorldEvents.LightingModeChanged, oldMode, fromServer); +} + + +/*########################################################################################################################* +*----------------------------------------------------Classic lighting-----------------------------------------------------* +*#########################################################################################################################*/ +static cc_int16* classic_heightmap; +#define HEIGHT_UNCALCULATED Int16_MaxValue + +#define ClassicLighting_CalcBody(get_block)\ +for (y = maxY; y >= 0; y--, i -= World.OneY) {\ + block = get_block;\ +\ + if (Blocks.BlocksLight[block]) {\ + offset = (Blocks.LightOffset[block] >> LIGHT_FLAG_SHADES_FROM_BELOW) & 1;\ + classic_heightmap[hIndex] = y - offset;\ + return y - offset;\ + }\ +} + +static int ClassicLighting_CalcHeightAt(int x, int maxY, int z, int hIndex) { + int i = World_Pack(x, maxY, z); + BlockID block; + int y, offset; + +#ifndef EXTENDED_BLOCKS + ClassicLighting_CalcBody(World.Blocks[i]); +#else + if (World.IDMask <= 0xFF) { + ClassicLighting_CalcBody(World.Blocks[i]); + } else { + ClassicLighting_CalcBody(World.Blocks[i] | (World.Blocks2[i] << 8)); + } +#endif + + classic_heightmap[hIndex] = -10; + return -10; +} + +int ClassicLighting_GetLightHeight(int x, int z) { + int hIndex = Lighting_Pack(x, z); + int lightH = classic_heightmap[hIndex]; + return lightH == HEIGHT_UNCALCULATED ? ClassicLighting_CalcHeightAt(x, World.Height - 1, z, hIndex) : lightH; +} + +/* Outside color is same as sunlight color, so we reuse when possible */ +cc_bool ClassicLighting_IsLit(int x, int y, int z) { + return y > ClassicLighting_GetLightHeight(x, z); +} + +cc_bool ClassicLighting_IsLit_Fast(int x, int y, int z) { + return y > classic_heightmap[Lighting_Pack(x, z)]; +} + +static PackedCol ClassicLighting_Color(int x, int y, int z) { + if (!World_Contains(x, y, z)) return Env.SunCol; + return y > ClassicLighting_GetLightHeight(x, z) ? Env.SunCol : Env.ShadowCol; +} + +static PackedCol SmoothLighting_Color(int x, int y, int z) { + if (!World_Contains(x, y, z)) return Env.SunCol; + if (Blocks.Brightness[World_GetBlock(x, y, z)]) return Env.SunCol; + return y > ClassicLighting_GetLightHeight(x, z) ? Env.SunCol : Env.ShadowCol; +} + +static PackedCol ClassicLighting_Color_XSide(int x, int y, int z) { + if (!World_Contains(x, y, z)) return Env.SunXSide; + return y > ClassicLighting_GetLightHeight(x, z) ? Env.SunXSide : Env.ShadowXSide; +} + +static PackedCol ClassicLighting_Color_Sprite_Fast(int x, int y, int z) { + return y > classic_heightmap[Lighting_Pack(x, z)] ? Env.SunCol : Env.ShadowCol; +} + +static PackedCol ClassicLighting_Color_YMax_Fast(int x, int y, int z) { + return y > classic_heightmap[Lighting_Pack(x, z)] ? Env.SunCol : Env.ShadowCol; +} + +static PackedCol ClassicLighting_Color_YMin_Fast(int x, int y, int z) { + return y > classic_heightmap[Lighting_Pack(x, z)] ? Env.SunYMin : Env.ShadowYMin; +} + +static PackedCol ClassicLighting_Color_XSide_Fast(int x, int y, int z) { + return y > classic_heightmap[Lighting_Pack(x, z)] ? Env.SunXSide : Env.ShadowXSide; +} + +static PackedCol ClassicLighting_Color_ZSide_Fast(int x, int y, int z) { + return y > classic_heightmap[Lighting_Pack(x, z)] ? Env.SunZSide : Env.ShadowZSide; +} + +void ClassicLighting_Refresh(void) { + int i; + for (i = 0; i < World.Width * World.Length; i++) { + classic_heightmap[i] = HEIGHT_UNCALCULATED; + } +} + + +/*########################################################################################################################* +*----------------------------------------------------Lighting update------------------------------------------------------* +*#########################################################################################################################*/ +static void ClassicLighting_UpdateLighting(int x, int y, int z, BlockID oldBlock, BlockID newBlock, int index, int lightH) { + cc_bool didBlock = Blocks.BlocksLight[oldBlock]; + cc_bool nowBlocks = Blocks.BlocksLight[newBlock]; + int oldOffset = (Blocks.LightOffset[oldBlock] >> LIGHT_FLAG_SHADES_FROM_BELOW) & 1; + int newOffset = (Blocks.LightOffset[newBlock] >> LIGHT_FLAG_SHADES_FROM_BELOW) & 1; + BlockID above; + + /* Two cases we need to handle here: */ + if (didBlock == nowBlocks) { + if (!didBlock) return; /* a) both old and new block do not block light */ + if (oldOffset == newOffset) return; /* b) both blocks blocked light at the same Y coordinate */ + } + + if ((y - newOffset) >= lightH) { + if (nowBlocks) { + classic_heightmap[index] = y - newOffset; + } else { + /* Part of the column is now visible to light, we don't know how exactly how high it should be though. */ + /* However, we know that if the block Y was above or equal to old light height, then the new light height must be <= block Y */ + ClassicLighting_CalcHeightAt(x, y, z, index); + } + } else if (y == lightH && oldOffset == 0) { + /* For a solid block on top of an upside down slab, they will both have the same light height. */ + /* So we need to account for this particular case. */ + above = y == (World.Height - 1) ? BLOCK_AIR : World_GetBlock(x, y + 1, z); + if (Blocks.BlocksLight[above]) return; + + if (nowBlocks) { + classic_heightmap[index] = y - newOffset; + } else { + ClassicLighting_CalcHeightAt(x, y - 1, z, index); + } + } +} + +static cc_bool ClassicLighting_Needs(BlockID block, BlockID other) { + return Blocks.Draw[block] != DRAW_OPAQUE || Blocks.Draw[other] != DRAW_GAS; +} + +#define ClassicLighting_NeedsNeighourBody(get_block)\ +/* Update if any blocks in the chunk are affected by light change. */ \ +for (; y >= minY; y--, i -= World.OneY) {\ + other = get_block;\ + affected = y == nY ? ClassicLighting_Needs(block, other) : Blocks.Draw[other] != DRAW_GAS;\ + if (affected) return true;\ +} + +static cc_bool ClassicLighting_NeedsNeighour(BlockID block, int i, int minY, int y, int nY) { + BlockID other; + cc_bool affected; + +#ifndef EXTENDED_BLOCKS + ClassicLighting_NeedsNeighourBody(World.Blocks[i]); +#else + if (World.IDMask <= 0xFF) { + ClassicLighting_NeedsNeighourBody(World.Blocks[i]); + } else { + ClassicLighting_NeedsNeighourBody(World.Blocks[i] | (World.Blocks2[i] << 8)); + } +#endif + return false; +} + +static void ClassicLighting_ResetNeighbour(int x, int y, int z, BlockID block, int cx, int cy, int cz, int minCy, int maxCy) { + int minY, maxY; + + if (minCy == maxCy) { + minY = cy << CHUNK_SHIFT; + + if (ClassicLighting_NeedsNeighour(block, World_Pack(x, y, z), minY, y, y)) { + MapRenderer_RefreshChunk(cx, cy, cz); + } + } else { + for (cy = maxCy; cy >= minCy; cy--) { + minY = (cy << CHUNK_SHIFT); + maxY = (cy << CHUNK_SHIFT) + CHUNK_MAX; + if (maxY > World.MaxY) maxY = World.MaxY; + + if (ClassicLighting_NeedsNeighour(block, World_Pack(x, maxY, z), minY, maxY, y)) { + MapRenderer_RefreshChunk(cx, cy, cz); + } + } + } +} + +static void ClassicLighting_ResetColumn(int cx, int cy, int cz, int minCy, int maxCy) { + if (minCy == maxCy) { + MapRenderer_RefreshChunk(cx, cy, cz); + } else { + for (cy = maxCy; cy >= minCy; cy--) { + MapRenderer_RefreshChunk(cx, cy, cz); + } + } +} + +static void ClassicLighting_RefreshAffected(int x, int y, int z, BlockID block, int oldHeight, int newHeight) { + int cx = x >> CHUNK_SHIFT, bX = x & CHUNK_MASK; + int cy = y >> CHUNK_SHIFT, bY = y & CHUNK_MASK; + int cz = z >> CHUNK_SHIFT, bZ = z & CHUNK_MASK; + + /* NOTE: much faster to only update the chunks that are affected by the change in shadows, rather than the entire column. */ + int newCy = newHeight < 0 ? 0 : newHeight >> 4; + int oldCy = oldHeight < 0 ? 0 : oldHeight >> 4; + int minCy = min(oldCy, newCy), maxCy = max(oldCy, newCy); + ClassicLighting_ResetColumn(cx, cy, cz, minCy, maxCy); + + if (bX == 0 && cx > 0) { + ClassicLighting_ResetNeighbour(x - 1, y, z, block, cx - 1, cy, cz, minCy, maxCy); + } + if (bY == 0 && cy > 0 && ClassicLighting_Needs(block, World_GetBlock(x, y - 1, z))) { + MapRenderer_RefreshChunk(cx, cy - 1, cz); + } + if (bZ == 0 && cz > 0) { + ClassicLighting_ResetNeighbour(x, y, z - 1, block, cx, cy, cz - 1, minCy, maxCy); + } + + if (bX == 15 && cx < World.ChunksX - 1) { + ClassicLighting_ResetNeighbour(x + 1, y, z, block, cx + 1, cy, cz, minCy, maxCy); + } + if (bY == 15 && cy < World.ChunksY - 1 && ClassicLighting_Needs(block, World_GetBlock(x, y + 1, z))) { + MapRenderer_RefreshChunk(cx, cy + 1, cz); + } + if (bZ == 15 && cz < World.ChunksZ - 1) { + ClassicLighting_ResetNeighbour(x, y, z + 1, block, cx, cy, cz + 1, minCy, maxCy); + } +} + +void ClassicLighting_OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock) { + int hIndex = Lighting_Pack(x, z); + int lightH = classic_heightmap[hIndex]; + int newHeight; + + /* Since light wasn't checked to begin with, means column never had meshes for any of its chunks built. */ + /* So we don't need to do anything. */ + if (lightH == HEIGHT_UNCALCULATED) return; + + ClassicLighting_UpdateLighting(x, y, z, oldBlock, newBlock, hIndex, lightH); + newHeight = classic_heightmap[hIndex] + 1; + ClassicLighting_RefreshAffected(x, y, z, newBlock, lightH + 1, newHeight); +} + + +/*########################################################################################################################* +*---------------------------------------------------Lighting heightmap----------------------------------------------------* +*#########################################################################################################################*/ +static int Heightmap_InitialCoverage(int x1, int z1, int xCount, int zCount, int* skip) { + int elemsLeft = 0, index = 0, curRunCount = 0; + int x, z, hIndex, lightH; + + for (z = 0; z < zCount; z++) { + hIndex = Lighting_Pack(x1, z1 + z); + for (x = 0; x < xCount; x++) { + lightH = classic_heightmap[hIndex++]; + + skip[index] = 0; + if (lightH == HEIGHT_UNCALCULATED) { + elemsLeft++; + curRunCount = 0; + } else { + skip[index - curRunCount]++; + curRunCount++; + } + index++; + } + curRunCount = 0; /* We can only skip an entire X row at most. */ + } + return elemsLeft; +} + +#define Heightmap_CalculateBody(get_block)\ +for (y = World.Height - 1; y >= 0; y--) {\ + if (elemsLeft <= 0) { return true; } \ + mapIndex = World_Pack(x1, y, z1);\ + hIndex = Lighting_Pack(x1, z1);\ +\ + for (z = 0; z < zCount; z++) {\ + baseIndex = mapIndex;\ + index = z * xCount;\ + for (x = 0; x < xCount;) {\ + curRunCount = skip[index];\ + x += curRunCount; mapIndex += curRunCount; index += curRunCount;\ +\ + if (x < xCount && Blocks.BlocksLight[get_block]) {\ + lightOffset = (Blocks.LightOffset[get_block] >> LIGHT_FLAG_SHADES_FROM_BELOW) & 1;\ + classic_heightmap[hIndex + x] = (cc_int16)(y - lightOffset);\ + elemsLeft--;\ + skip[index] = 0;\ +\ + offset = prevRunCount + curRunCount;\ + newRunCount = skip[index - offset] + 1;\ +\ + /* consider case 1 0 1 0, where we are at last 0 */ \ + /* we need to make this 3 0 0 0 and advance by 1 */ \ + oldRunCount = (x - offset + newRunCount) < xCount ? skip[index - offset + newRunCount] : 0; \ + if (oldRunCount != 0) {\ + skip[index - offset + newRunCount] = 0; \ + newRunCount += oldRunCount; \ + } \ + skip[index - offset] = newRunCount; \ + x += oldRunCount; index += oldRunCount; mapIndex += oldRunCount; \ + prevRunCount = newRunCount; \ + } else { \ + prevRunCount = 0; \ + }\ + x++; mapIndex++; index++; \ + }\ + prevRunCount = 0;\ + hIndex += World.Width;\ + mapIndex = baseIndex + World.Width; /* advance one Z */ \ + }\ +} + +static cc_bool Heightmap_CalculateCoverage(int x1, int z1, int xCount, int zCount, int elemsLeft, int* skip) { + int prevRunCount = 0, curRunCount, newRunCount, oldRunCount; + int lightOffset, offset; + int mapIndex, hIndex, baseIndex, index; + int x, y, z; + +#ifndef EXTENDED_BLOCKS + Heightmap_CalculateBody(World.Blocks[mapIndex]); +#else + if (World.IDMask <= 0xFF) { + Heightmap_CalculateBody(World.Blocks[mapIndex]); + } else { + Heightmap_CalculateBody(World.Blocks[mapIndex] | (World.Blocks2[mapIndex] << 8)); + } +#endif + return false; +} + +static void Heightmap_FinishCoverage(int x1, int z1, int xCount, int zCount) { + int x, z, hIndex, lightH; + + for (z = 0; z < zCount; z++) { + hIndex = Lighting_Pack(x1, z1 + z); + for (x = 0; x < xCount; x++, hIndex++) { + lightH = classic_heightmap[hIndex]; + + if (lightH == HEIGHT_UNCALCULATED) { + classic_heightmap[hIndex] = -10; + } + } + } +} + + +void ClassicLighting_LightHint(int startX, int startY, int startZ) { + int x1 = max(startX, 0), x2 = min(World.Width, startX + EXTCHUNK_SIZE); + int z1 = max(startZ, 0), z2 = min(World.Length, startZ + EXTCHUNK_SIZE); + int xCount = x2 - x1, zCount = z2 - z1; + int skip[EXTCHUNK_SIZE * EXTCHUNK_SIZE]; + + int elemsLeft = Heightmap_InitialCoverage(x1, z1, xCount, zCount, skip); + if (!Heightmap_CalculateCoverage(x1, z1, xCount, zCount, elemsLeft, skip)) { + Heightmap_FinishCoverage(x1, z1, xCount, zCount); + } +} + +void ClassicLighting_FreeState(void) { + Mem_Free(classic_heightmap); + classic_heightmap = NULL; +} + +void ClassicLighting_AllocState(void) { + classic_heightmap = (cc_int16*)Mem_TryAlloc(World.Width * World.Length, 2); + if (classic_heightmap) { + ClassicLighting_Refresh(); + } else { + World_OutOfMemory(); + } +} + +static void ClassicLighting_SetActive(void) { + cc_bool smoothLighting = false; + if (!Game_ClassicMode) smoothLighting = Options_GetBool(OPT_SMOOTH_LIGHTING, false); + + Lighting.OnBlockChanged = ClassicLighting_OnBlockChanged; + Lighting.Refresh = ClassicLighting_Refresh; + Lighting.IsLit = ClassicLighting_IsLit; + Lighting.Color = smoothLighting ? SmoothLighting_Color : ClassicLighting_Color; + Lighting.Color_XSide = ClassicLighting_Color_XSide; + + Lighting.IsLit_Fast = ClassicLighting_IsLit_Fast; + Lighting.Color_Sprite_Fast = ClassicLighting_Color_Sprite_Fast; + Lighting.Color_YMax_Fast = ClassicLighting_Color_YMax_Fast; + Lighting.Color_YMin_Fast = ClassicLighting_Color_YMin_Fast; + Lighting.Color_XSide_Fast = ClassicLighting_Color_XSide_Fast; + Lighting.Color_ZSide_Fast = ClassicLighting_Color_ZSide_Fast; + + Lighting.FreeState = ClassicLighting_FreeState; + Lighting.AllocState = ClassicLighting_AllocState; + Lighting.LightHint = ClassicLighting_LightHint; +} + + +/*########################################################################################################################* +*---------------------------------------------------Lighting component----------------------------------------------------* +*#########################################################################################################################*/ +static void Lighting_ApplyActive(void) { + if (Lighting_Mode != LIGHTING_MODE_CLASSIC) { + FancyLighting_SetActive(); + } else { + ClassicLighting_SetActive(); + } +} + +static void Lighting_SwitchActive(void) { + Lighting.FreeState(); + Lighting_ApplyActive(); + Lighting.AllocState(); +} + +static void Lighting_HandleModeChanged(void* obj, cc_uint8 oldMode, cc_bool fromServer) { + if (Lighting_Mode == oldMode) return; + Builder_ApplyActive(); + + if (World.Loaded) { + Lighting_SwitchActive(); + MapRenderer_Refresh(); + } else { + Lighting_ApplyActive(); + } +} + +static void OnInit(void) { + Lighting_Mode = Options_GetEnum(OPT_LIGHTING_MODE, LIGHTING_MODE_CLASSIC, LightingMode_Names, LIGHTING_MODE_COUNT); + Lighting_ModeLockedByServer = false; + Lighting_ModeSetByServer = false; + Lighting_ModeUserCached = Lighting_Mode; + + FancyLighting_OnInit(); + Lighting_ApplyActive(); + + Event_Register_(&WorldEvents.LightingModeChanged, NULL, Lighting_HandleModeChanged); +} +static void OnReset(void) { Lighting.FreeState(); } +static void OnNewMapLoaded(void) { Lighting.AllocState(); } + +struct IGameComponent Lighting_Component = { + OnInit, /* Init */ + OnReset, /* Free */ + OnReset, /* Reset */ + OnReset, /* OnNewMap */ + OnNewMapLoaded /* OnNewMapLoaded */ +}; \ No newline at end of file diff --git a/src/Lighting.h b/src/Lighting.h new file mode 100644 index 0000000..0219088 --- /dev/null +++ b/src/Lighting.h @@ -0,0 +1,88 @@ +#ifndef CC_WORLDLIGHTING_H +#define CC_WORLDLIGHTING_H +#include "PackedCol.h" +/* +Abstracts lighting of blocks in the world + Built-in lighting engines: + - ClassicLighting: Uses a simple heightmap, where each block is either in sun or shadow + +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent Lighting_Component; + +enum LightingMode { + LIGHTING_MODE_CLASSIC, LIGHTING_MODE_FANCY, LIGHTING_MODE_COUNT +}; +extern const char* const LightingMode_Names[LIGHTING_MODE_COUNT]; +extern cc_uint8 Lighting_Mode; + +extern cc_bool Lighting_ModeLockedByServer; +/* True if the current lighting mode has been set by the server instead of the client */ +extern cc_bool Lighting_ModeSetByServer; +/* The lighting mode that was set by the client before being set by the server */ +extern cc_uint8 Lighting_ModeUserCached; +void Lighting_SetMode(cc_uint8 mode, cc_bool fromServer); + + +/* How much ambient occlusion to apply in fancy lighting where 1.0f = none and 0.0f = maximum*/ +#define FANCY_AO 0.5F +/* How many unique "levels" of light there are when fancy lighting is used. */ +#define FANCY_LIGHTING_LEVELS 16 +#define FANCY_LIGHTING_MAX_LEVEL (FANCY_LIGHTING_LEVELS - 1) +/* How many bits to shift lamplight level to the left when storing it in a byte along with lavalight level. */ +#define FANCY_LIGHTING_LAMP_SHIFT 4 +/* A byte that fills the lamp level area with ones. Equivalent to 0b_1111_0000 */ +#define FANCY_LIGHTING_LAMP_MASK 0xF0 + +CC_VAR extern struct _Lighting { + /* Releases/Frees the per-level lighting state */ + void (*FreeState)(void); + /* Allocates the per-level lighting state */ + /* (called after map has been fully loaded) */ + void (*AllocState)(void); + /* Quickly calculates lighting for the blocks in the region */ + /* [x, y, z] to [x + 18, y + 18, z + 18] */ + void (*LightHint)(int startX, int startY, int startZ); + + /* Called when a block is changed to update internal lighting state. */ + /* NOTE: Implementations ***MUST*** mark all chunks affected by this lighting change as needing to be refreshed. */ + void (*OnBlockChanged)(int x, int y, int z, BlockID oldBlock, BlockID newBlock); + /* Invalidates/Resets lighting state for all of the blocks in the world */ + /* (e.g. because a block changed whether it is full bright or not) */ + void (*Refresh)(void); + + /* Returns whether the block at the given coordinates is fully in sunlight. */ + /* NOTE: Does ***NOT*** check that the coordinates are inside the map. */ + cc_bool (*IsLit)(int x, int y, int z); + /* Returns the light colour at the given coordinates. */ + PackedCol (*Color)(int x, int y, int z); + /* Returns the light colour at the given coordinates. */ + PackedCol (*Color_XSide)(int x, int y, int z); + + /* _Fast functions assume Lighting_LightHint has already been called */ + /* and performed the necessary calculations for the given x/z coords */ + /* _Fast functions also do NOT check coordinates are inside the map */ + + cc_bool (*IsLit_Fast)(int x, int y, int z); + PackedCol (*Color_Sprite_Fast)(int x, int y, int z); + PackedCol (*Color_YMax_Fast)(int x, int y, int z); + PackedCol (*Color_YMin_Fast)(int x, int y, int z); + PackedCol (*Color_XSide_Fast)(int x, int y, int z); + PackedCol (*Color_ZSide_Fast)(int x, int y, int z); +} Lighting; + +void FancyLighting_SetActive(void); +void FancyLighting_OnInit(void); + +/* Expose ClassicLighting functions for reuse in Fancy lighting */ +void ClassicLighting_Refresh(void); +void ClassicLighting_FreeState(void); +void ClassicLighting_AllocState(void); +int ClassicLighting_GetLightHeight(int x, int z); +void ClassicLighting_LightHint(int startX, int startY, int startZ); +cc_bool ClassicLighting_IsLit(int x, int y, int z); +cc_bool ClassicLighting_IsLit_Fast(int x, int y, int z); +void ClassicLighting_OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock); + +#endif diff --git a/src/Logger.c b/src/Logger.c new file mode 100644 index 0000000..d3107a4 --- /dev/null +++ b/src/Logger.c @@ -0,0 +1,1386 @@ +#include "Logger.h" +#include "String.h" +#include "Platform.h" +#include "Window.h" +#include "Funcs.h" +#include "Stream.h" +#include "Errors.h" +#include "Utils.h" + +#if defined CC_BUILD_WEB + /* Can't see native CPU state with javascript */ +#elif defined CC_BUILD_WIN + #define WIN32_LEAN_AND_MEAN + #define NOSERVICE + #define NOMCX + #define NOIME + #define CUR_PROCESS_HANDLE ((HANDLE)-1) /* GetCurrentProcess() always returns -1 */ + + #include + #include + static HANDLE curProcess = CUR_PROCESS_HANDLE; +#elif defined CC_BUILD_OPENBSD || defined CC_BUILD_HAIKU || defined CC_BUILD_SERENITY + #include + /* These operating systems don't provide sys/ucontext.h */ + /* But register constants be found from includes in */ + #elif defined CC_BUILD_OS2 + #include + #include <386/ucontext.h> +#elif defined CC_BUILD_LINUX || defined CC_BUILD_ANDROID + /* Need to define this to get REG_ constants */ + #define _GNU_SOURCE + #include + #include +#elif defined CC_BUILD_POSIX + #include + #include +#endif + +#ifdef CC_BUILD_DARWIN +/* Need this to detect macOS < 10.4, and switch to NS* api instead if so */ +#include +#endif +/* Only show up to 50 frames in backtrace */ +#define MAX_BACKTRACE_FRAMES 50 + +static void AbortCommon(cc_result result, const char* raw_msg, void* ctx); + + +/*########################################################################################################################* +*----------------------------------------------------------Warning--------------------------------------------------------* +*#########################################################################################################################*/ +void Logger_DialogWarn(const cc_string* msg) { + cc_string dst; char dstBuffer[512]; + String_InitArray_NT(dst, dstBuffer); + + String_Copy(&dst, msg); + dst.buffer[dst.length] = '\0'; + Window_ShowDialog(Logger_DialogTitle, dst.buffer); +} +const char* Logger_DialogTitle = "Error"; +Logger_DoWarn Logger_WarnFunc = Logger_DialogWarn; + +/* Returns a description for some ClassiCube specific error codes */ +static const char* GetCCErrorDesc(cc_result res) { + switch (res) { + case ERR_END_OF_STREAM: return "End of stream"; + case ERR_NOT_SUPPORTED: return "Operation not supported"; + case ERR_INVALID_ARGUMENT: return "Invalid argument"; + case ERR_OUT_OF_MEMORY: return "Out of memory"; + + case OGG_ERR_INVALID_SIG: return "Only OGG music files supported"; + case OGG_ERR_VERSION: return "Invalid OGG format version"; + case AUDIO_ERR_MP3_SIG: return "MP3 audio files are not supported"; + + case WAV_ERR_STREAM_HDR: return "Only WAV sound files supported"; + case WAV_ERR_STREAM_TYPE: return "Invalid WAV type"; + case WAV_ERR_DATA_TYPE: return "Unsupported WAV audio format"; + + case ZIP_ERR_TOO_MANY_ENTRIES: return "Cannot load .zip files with over 1024 entries"; + + case PNG_ERR_INVALID_SIG: return "Only PNG images supported"; + case PNG_ERR_INVALID_HDR_SIZE: return "Invalid PNG header size"; + case PNG_ERR_TOO_WIDE: return "PNG image too wide"; + case PNG_ERR_TOO_TALL: return "PNG image too tall"; + case PNG_ERR_INTERLACED: return "Interlaced PNGs unsupported"; + case PNG_ERR_REACHED_IEND: return "Incomplete PNG image data"; + case PNG_ERR_NO_DATA: return "No image in PNG"; + case PNG_ERR_INVALID_SCANLINE: return "Invalid PNG scanline type"; + case PNG_ERR_16BITSAMPLES: return "16 bpp PNGs unsupported"; + + case NBT_ERR_UNKNOWN: return "Unknown NBT tag type"; + case CW_ERR_ROOT_TAG: return "Invalid root NBT tag"; + case CW_ERR_STRING_LEN: return "NBT string too long"; + + case ERR_DOWNLOAD_INVALID: return "Website denied download or doesn't exist"; + case ERR_NO_AUDIO_OUTPUT: return "No audio output devices plugged in"; + case ERR_INVALID_DATA_URL: return "Cannot download from invalid URL"; + case ERR_INVALID_OPEN_URL: return "Cannot navigate to invalid URL"; + + case NBT_ERR_EXPECTED_I8: return "Expected Int8 NBT tag"; + case NBT_ERR_EXPECTED_I16: return "Expected Int16 NBT tag"; + case NBT_ERR_EXPECTED_I32: return "Expected Int32 NBT tag"; + case NBT_ERR_EXPECTED_F32: return "Expected Float32 NBT tag"; + case NBT_ERR_EXPECTED_STR: return "Expected String NBT tag"; + case NBT_ERR_EXPECTED_ARR: return "Expected ByteArray NBT tag"; + case NBT_ERR_ARR_TOO_SMALL:return "ByteArray NBT tag too small"; + + case HTTP_ERR_NO_SSL: return "HTTPS URLs are not currently supported"; + case SOCK_ERR_UNKNOWN_HOST: return "Host could not be resolved to an IP address"; + } + return NULL; +} + +/* Appends more detailed information about an error if possible */ +static void AppendErrorDesc(cc_string* msg, cc_result res, Logger_DescribeError describeErr) { + const char* cc_err; + cc_string err; char errBuffer[128]; + String_InitArray(err, errBuffer); + + cc_err = GetCCErrorDesc(res); + if (cc_err) { + String_Format1(msg, "\n Error meaning: %c", cc_err); + } else if (describeErr(res, &err)) { + String_Format1(msg, "\n Error meaning: %s", &err); + } +} + +void Logger_FormatWarn(cc_string* msg, cc_result res, const char* action, Logger_DescribeError describeErr) { + String_Format2(msg, "Error %e when %c", &res, action); + AppendErrorDesc(msg, res, describeErr); +} + +void Logger_FormatWarn2(cc_string* msg, cc_result res, const char* action, const cc_string* path, Logger_DescribeError describeErr) { + String_Format3(msg, "Error %e when %c '%s'", &res, action, path); + AppendErrorDesc(msg, res, describeErr); +} + +static cc_bool DescribeSimple(cc_result res, cc_string* dst) { return false; } +void Logger_SimpleWarn(cc_result res, const char* action) { + Logger_Warn(res, action, DescribeSimple); +} +void Logger_SimpleWarn2(cc_result res, const char* action, const cc_string* path) { + Logger_Warn2(res, action, path, DescribeSimple); +} + +void Logger_Warn(cc_result res, const char* action, Logger_DescribeError describeErr) { + cc_string msg; char msgBuffer[256]; + String_InitArray(msg, msgBuffer); + + Logger_FormatWarn(&msg, res, action, describeErr); + Logger_WarnFunc(&msg); +} + +void Logger_Warn2(cc_result res, const char* action, const cc_string* path, Logger_DescribeError describeErr) { + cc_string msg; char msgBuffer[256]; + String_InitArray(msg, msgBuffer); + + Logger_FormatWarn2(&msg, res, action, path, describeErr); + Logger_WarnFunc(&msg); +} + +void Logger_DynamicLibWarn(const char* action, const cc_string* path) { + cc_string err; char errBuffer[128]; + cc_string msg; char msgBuffer[256]; + String_InitArray(msg, msgBuffer); + String_InitArray(err, errBuffer); + + String_Format2(&msg, "Error %c '%s'", action, path); + if (DynamicLib_DescribeError(&err)) { + String_Format1(&msg, ":\n %s", &err); + } + Logger_WarnFunc(&msg); +} + +void Logger_SysWarn(cc_result res, const char* action) { + Logger_Warn(res, action, Platform_DescribeError); +} +void Logger_SysWarn2(cc_result res, const char* action, const cc_string* path) { + Logger_Warn2(res, action, path, Platform_DescribeError); +} + + +/*########################################################################################################################* +*------------------------------------------------------Frame dumping------------------------------------------------------* +*#########################################################################################################################*/ +static void PrintFrame(cc_string* str, cc_uintptr addr, cc_uintptr symAddr, const char* symName, const char* modName) { + cc_string module; + int offset; + if (!modName) modName = "???"; + + module = String_FromReadonly(modName); + Utils_UNSAFE_GetFilename(&module); + String_Format2(str, "%x - %s", &addr, &module); + + if (symName && symName[0]) { + offset = (int)(addr - symAddr); + String_Format2(str, "(%c+%i)" _NL, symName, &offset); + } else { + String_AppendConst(str, _NL); + } +} + +#if defined CC_BUILD_WIN +struct SymbolAndName { IMAGEHLP_SYMBOL symbol; char name[256]; }; +static void DumpFrame(HANDLE process, cc_string* trace, cc_uintptr addr) { + char strBuffer[512]; cc_string str; + String_InitArray(str, strBuffer); + + struct SymbolAndName s = { 0 }; + s.symbol.MaxNameLength = 255; + s.symbol.SizeOfStruct = sizeof(IMAGEHLP_SYMBOL); + SymGetSymFromAddr(process, addr, NULL, &s.symbol); + + IMAGEHLP_MODULE m = { 0 }; + m.SizeOfStruct = sizeof(IMAGEHLP_MODULE); + SymGetModuleInfo(process, addr, &m); + + PrintFrame(&str, addr, s.symbol.Address, s.symbol.Name, m.ModuleName); + String_AppendString(trace, &str); + + /* This function only works for .pdb debug info anyways */ + /* This function is also missing on Windows 9X */ +#if _MSC_VER + IMAGEHLP_LINE line = { 0 }; DWORD lineOffset; + line.SizeOfStruct = sizeof(IMAGEHLP_LINE); + if (SymGetLineFromAddr(process, addr, &lineOffset, &line)) { + String_Format2(&str, " line %i in %c\r\n", &line.LineNumber, line.FileName); + } +#endif + Logger_Log(&str); +} +#elif defined MAC_OS_X_VERSION_MIN_REQUIRED && (MAC_OS_X_VERSION_MIN_REQUIRED < 1040) +/* dladdr does not exist prior to macOS tiger */ +static void DumpFrame(cc_string* trace, void* addr) { + cc_string str; char strBuffer[384]; + String_InitArray(str, strBuffer); + /* alas NSModuleForSymbol doesn't work with raw addresses */ + + PrintFrame(&str, (cc_uintptr)addr, 0, NULL, NULL); + String_AppendString(trace, &str); + Logger_Log(&str); +} +#elif defined CC_BUILD_IRIX +/* IRIX doesn't expose a nice interface for dladdr */ +static void DumpFrame(cc_string* trace, void* addr) { + cc_uintptr addr_ = (cc_uintptr)addr; + String_Format1(trace, "%x", &addr_); +} +#elif defined CC_BUILD_POSIX && !defined CC_BUILD_OS2 +/* need to define __USE_GNU for dladdr */ +#ifndef __USE_GNU +#define __USE_GNU +#endif +#include +#undef __USE_GNU + +static void DumpFrame(cc_string* trace, void* addr) { + cc_string str; char strBuffer[384]; + Dl_info s; + + String_InitArray(str, strBuffer); + s.dli_sname = NULL; + s.dli_fname = NULL; + dladdr(addr, &s); + + PrintFrame(&str, (cc_uintptr)addr, (cc_uintptr)s.dli_saddr, s.dli_sname, s.dli_fname); + String_AppendString(trace, &str); + Logger_Log(&str); +} +#else +/* No backtrace support implemented */ +#endif + + +/*########################################################################################################################* +*-------------------------------------------------------Backtracing-------------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_WIN +/* This callback function is used so stack Walking works using StackWalk properly on Windows 9x: */ +/* - on Windows 9x process ID is passed instead of process handle as the "process" argument */ +/* - the SymXYZ functions expect a process ID on Windows 9x, so that works fine */ +/* - if NULL is passed as the "ReadMemory" argument, the default callback using ReadProcessMemory is used */ +/* - however, ReadProcessMemory expects a process handle, and so that will fail since it's given a process ID */ +/* So to work around this, instead manually call ReadProcessMemory with the current process handle */ +static BOOL __stdcall ReadMemCallback(HANDLE process, DWORD_PTR baseAddress, PVOID buffer, DWORD size, PDWORD numBytesRead) { + SIZE_T numRead = 0; + BOOL ok = ReadProcessMemory(CUR_PROCESS_HANDLE, (LPCVOID)baseAddress, buffer, size, &numRead); + + *numBytesRead = (DWORD)numRead; /* DWORD always 32 bits */ + return ok; +} +static cc_uintptr spRegister; + +static int GetFrames(CONTEXT* ctx, cc_uintptr* addrs, int max) { + STACKFRAME frame = { 0 }; + int count, type; + HANDLE thread; + + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Mode = AddrModeFlat; + +#if defined _M_IX86 + type = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = ctx->Eip; + frame.AddrFrame.Offset = ctx->Ebp; + frame.AddrStack.Offset = ctx->Esp; + spRegister = ctx->Esp; +#elif defined _M_X64 + type = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = ctx->Rip; + frame.AddrFrame.Offset = ctx->Rsp; + frame.AddrStack.Offset = ctx->Rsp; + spRegister = ctx->Rsp; +#else + /* Always available after XP, so use that */ + return RtlCaptureStackBackTrace(0, max, (void**)addrs, NULL); +#endif + thread = GetCurrentThread(); + + for (count = 0; count < max; count++) + { + if (!StackWalk(type, curProcess, thread, &frame, ctx, ReadMemCallback, SymFunctionTableAccess, SymGetModuleBase, NULL)) break; + if (!frame.AddrFrame.Offset) break; + addrs[count] = frame.AddrPC.Offset; + } + return count; +} + +void Logger_Backtrace(cc_string* trace, void* ctx) { + cc_uintptr addrs[MAX_BACKTRACE_FRAMES]; + int i, frames; + + SymInitialize(curProcess, NULL, TRUE); /* TODO only in MSVC.. */ + frames = GetFrames((CONTEXT*)ctx, addrs, MAX_BACKTRACE_FRAMES); + + for (i = 0; i < frames; i++) { + DumpFrame(curProcess, trace, addrs[i]); + } + String_AppendConst(trace, _NL); +} +#elif defined CC_BUILD_ANDROID + #define CC_BACKTRACE_UNWIND +#elif defined CC_BUILD_DARWIN +/* backtrace is only available on macOS since 10.5 */ +void Logger_Backtrace(cc_string* trace, void* ctx) { + void* addrs[MAX_BACKTRACE_FRAMES]; + unsigned i, frames; + /* See lldb/tools/debugserver/source/MacOSX/stack_logging.h */ + /* backtrace uses this internally too, and exists since macOS 10.1 */ + extern void thread_stack_pcs(void** buffer, unsigned max, unsigned* nb); + + thread_stack_pcs(addrs, MAX_BACKTRACE_FRAMES, &frames); + /* Skip frames don't want to include in backtrace */ + /* frame 0 = thread_stack_pcs */ + /* frame 1 = Logger_Backtrace */ + for (i = 2; i < frames; i++) + { + DumpFrame(trace, addrs[i]); + } + String_AppendConst(trace, _NL); +} +#elif defined CC_BUILD_SERENITY +void Logger_Backtrace(cc_string* trace, void* ctx) { + String_AppendConst(trace, "-- backtrace unimplemented --"); + /* TODO: Backtrace using LibSymbolication */ +} +#elif defined CC_BUILD_IRIX +void Logger_Backtrace(cc_string* trace, void* ctx) { + String_AppendConst(trace, "-- backtrace unimplemented --"); + /* TODO implement backtrace using exc_unwind https://nixdoc.net/man-pages/IRIX/man3/exception.3.html */ +} +#elif defined CC_BACKTRACE_BUILTIN +/* Implemented later at end of the file */ +#elif defined CC_BUILD_POSIX && defined _GLIBC_ +#include +void Logger_Backtrace(cc_string* trace, void* ctx) { + void* addrs[MAX_BACKTRACE_FRAMES]; + int i, frames = backtrace(addrs, MAX_BACKTRACE_FRAMES); + + for (i = 0; i < frames; i++) + { + DumpFrame(trace, addrs[i]); + } + String_AppendConst(trace, _NL); +} +#elif defined CC_BUILD_POSIX +/* musl etc - rely on unwind from GCC instead */ + #define CC_BACKTRACE_UNWIND +#else +void Logger_Backtrace(cc_string* trace, void* ctx) { } +#endif + + +/*########################################################################################################################* +*-----------------------------------------------------CPU registers-------------------------------------------------------* +*#########################################################################################################################*/ +/* Unfortunately, operating systems vary wildly in how they name and access registers for dumping */ +/* So this is the simplest way to avoid duplicating code on each platform */ +#define Dump_X86() \ +String_Format3(str, "eax=%x ebx=%x ecx=%x" _NL, REG_GET(ax,AX), REG_GET(bx,BX), REG_GET(cx,CX));\ +String_Format3(str, "edx=%x esi=%x edi=%x" _NL, REG_GET(dx,DX), REG_GET(si,SI), REG_GET(di,DI));\ +String_Format3(str, "eip=%x ebp=%x esp=%x" _NL, REG_GET(ip,IP), REG_GET(bp,BP), REG_GET(sp,SP)); + +#define Dump_X64() \ +String_Format3(str, "rax=%x rbx=%x rcx=%x" _NL, REG_GET(ax,AX), REG_GET(bx,BX), REG_GET(cx,CX));\ +String_Format3(str, "rdx=%x rsi=%x rdi=%x" _NL, REG_GET(dx,DX), REG_GET(si,SI), REG_GET(di,DI));\ +String_Format3(str, "rip=%x rbp=%x rsp=%x" _NL, REG_GET(ip,IP), REG_GET(bp,BP), REG_GET(sp,SP));\ +String_Format3(str, "r8 =%x r9 =%x r10=%x" _NL, REG_GET(8,8), REG_GET(9,9), REG_GET(10,10));\ +String_Format3(str, "r11=%x r12=%x r13=%x" _NL, REG_GET(11,11), REG_GET(12,12), REG_GET(13,13));\ +String_Format2(str, "r14=%x r15=%x" _NL, REG_GET(14,14), REG_GET(15,15)); + +#define Dump_PPC() \ +String_Format4(str, "r0 =%x r1 =%x r2 =%x r3 =%x" _NL, REG_GNUM(0), REG_GNUM(1), REG_GNUM(2), REG_GNUM(3)); \ +String_Format4(str, "r4 =%x r5 =%x r6 =%x r7 =%x" _NL, REG_GNUM(4), REG_GNUM(5), REG_GNUM(6), REG_GNUM(7)); \ +String_Format4(str, "r8 =%x r9 =%x r10=%x r11=%x" _NL, REG_GNUM(8), REG_GNUM(9), REG_GNUM(10), REG_GNUM(11)); \ +String_Format4(str, "r12=%x r13=%x r14=%x r15=%x" _NL, REG_GNUM(12), REG_GNUM(13), REG_GNUM(14), REG_GNUM(15)); \ +String_Format4(str, "r16=%x r17=%x r18=%x r19=%x" _NL, REG_GNUM(16), REG_GNUM(17), REG_GNUM(18), REG_GNUM(19)); \ +String_Format4(str, "r20=%x r21=%x r22=%x r23=%x" _NL, REG_GNUM(20), REG_GNUM(21), REG_GNUM(22), REG_GNUM(23)); \ +String_Format4(str, "r24=%x r25=%x r26=%x r27=%x" _NL, REG_GNUM(24), REG_GNUM(25), REG_GNUM(26), REG_GNUM(27)); \ +String_Format4(str, "r28=%x r29=%x r30=%x r31=%x" _NL, REG_GNUM(28), REG_GNUM(29), REG_GNUM(30), REG_GNUM(31)); \ +String_Format3(str, "pc =%x lr =%x ctr=%x" _NL, REG_GET_PC(), REG_GET_LR(), REG_GET_CTR()); + +#define Dump_ARM32() \ +String_Format3(str, "r0 =%x r1 =%x r2 =%x" _NL, REG_GNUM(0), REG_GNUM(1), REG_GNUM(2));\ +String_Format3(str, "r3 =%x r4 =%x r5 =%x" _NL, REG_GNUM(3), REG_GNUM(4), REG_GNUM(5));\ +String_Format3(str, "r6 =%x r7 =%x r8 =%x" _NL, REG_GNUM(6), REG_GNUM(7), REG_GNUM(8));\ +String_Format3(str, "r9 =%x r10=%x fp =%x" _NL, REG_GNUM(9), REG_GNUM(10), REG_GET_FP());\ +String_Format3(str, "ip =%x sp =%x lr =%x" _NL, REG_GET_IP(), REG_GET_SP(), REG_GET_LR());\ +String_Format1(str, "pc =%x" _NL, REG_GET_PC()); + +#define Dump_ARM64() \ +String_Format4(str, "r0 =%x r1 =%x r2 =%x r3 =%x" _NL, REG_GNUM(0), REG_GNUM(1), REG_GNUM(2), REG_GNUM(3)); \ +String_Format4(str, "r4 =%x r5 =%x r6 =%x r7 =%x" _NL, REG_GNUM(4), REG_GNUM(5), REG_GNUM(6), REG_GNUM(7)); \ +String_Format4(str, "r8 =%x r9 =%x r10=%x r11=%x" _NL, REG_GNUM(8), REG_GNUM(9), REG_GNUM(10), REG_GNUM(11)); \ +String_Format4(str, "r12=%x r13=%x r14=%x r15=%x" _NL, REG_GNUM(12), REG_GNUM(13), REG_GNUM(14), REG_GNUM(15)); \ +String_Format4(str, "r16=%x r17=%x r18=%x r19=%x" _NL, REG_GNUM(16), REG_GNUM(17), REG_GNUM(18), REG_GNUM(19)); \ +String_Format4(str, "r20=%x r21=%x r22=%x r23=%x" _NL, REG_GNUM(20), REG_GNUM(21), REG_GNUM(22), REG_GNUM(23)); \ +String_Format4(str, "r24=%x r25=%x r26=%x r27=%x" _NL, REG_GNUM(24), REG_GNUM(25), REG_GNUM(26), REG_GNUM(27)); \ +String_Format4(str, "r28=%x fp =%x lr =%x sp =%x" _NL, REG_GNUM(28), REG_GET_FP(), REG_GET_LR(), REG_GET_SP()); \ +String_Format1(str, "pc =%x" _NL, REG_GET_PC()); + +#define Dump_Alpha() \ +String_Format4(str, "v0 =%x t0 =%x t1 =%x t2 =%x" _NL, REG_GNUM(0), REG_GNUM(1), REG_GNUM(2), REG_GNUM(3)); \ +String_Format4(str, "t3 =%x t4 =%x t5 =%x t6 =%x" _NL, REG_GNUM(4), REG_GNUM(5), REG_GNUM(6), REG_GNUM(7)); \ +String_Format4(str, "t7 =%x s0 =%x s1 =%x s2 =%x" _NL, REG_GNUM(8), REG_GNUM(9), REG_GNUM(10), REG_GNUM(11)); \ +String_Format4(str, "s3 =%x s4 =%x s5 =%x a0 =%x" _NL, REG_GNUM(12), REG_GNUM(13), REG_GNUM(14), REG_GNUM(16)); \ +String_Format4(str, "a1 =%x a2 =%x a3 =%x a4 =%x" _NL, REG_GNUM(17), REG_GNUM(18), REG_GNUM(19), REG_GNUM(20)); \ +String_Format4(str, "a5 =%x t8 =%x t9 =%x t10=%x" _NL, REG_GNUM(21), REG_GNUM(22), REG_GNUM(23), REG_GNUM(24)); \ +String_Format4(str, "t11=%x ra =%x pv =%x at =%x" _NL, REG_GNUM(25), REG_GNUM(26), REG_GNUM(27), REG_GNUM(28)); \ +String_Format4(str, "gp =%x fp =%x sp =%x pc =%x" _NL, REG_GNUM(29), REG_GET_FP(), REG_GET_SP(), REG_GET_PC()); + +#define Dump_SPARC() \ +String_Format4(str, "o0=%x o1=%x o2=%x o3=%x" _NL, REG_GET(o0,O0), REG_GET(o1,O1), REG_GET(o2,O2), REG_GET(o3,O3)); \ +String_Format4(str, "o4=%x o5=%x o6=%x o7=%x" _NL, REG_GET(o4,O4), REG_GET(o5,O5), REG_GET(o6,O6), REG_GET(o7,O7)); \ +String_Format4(str, "g1=%x g2=%x g3=%x g4=%x" _NL, REG_GET(g1,G1), REG_GET(g2,G2), REG_GET(g3,G3), REG_GET(g4,G4)); \ +String_Format4(str, "g5=%x g6=%x g7=%x y =%x" _NL, REG_GET(g5,G5), REG_GET(g6,G6), REG_GET(g7,G7), REG_GET( y, Y)); \ +String_Format2(str, "pc=%x nc=%x" _NL, REG_GET(pc,PC), REG_GET(npc,nPC)); + +#define Dump_RISCV() \ +String_Format4(str, "pc =%x r1 =%x r2 =%x r3 =%x" _NL, REG_GET_PC(), REG_GNUM(1), REG_GNUM(2), REG_GNUM(3)); \ +String_Format4(str, "r4 =%x r5 =%x r6 =%x r7 =%x" _NL, REG_GNUM(4), REG_GNUM(5), REG_GNUM(6), REG_GNUM(7)); \ +String_Format4(str, "r8 =%x r9 =%x r10=%x r11=%x" _NL, REG_GNUM(8), REG_GNUM(9), REG_GNUM(10), REG_GNUM(11)); \ +String_Format4(str, "r12=%x r13=%x r14=%x r15=%x" _NL, REG_GNUM(12), REG_GNUM(13), REG_GNUM(14), REG_GNUM(15)); \ +String_Format4(str, "r16=%x r17=%x r18=%x r19=%x" _NL, REG_GNUM(16), REG_GNUM(17), REG_GNUM(18), REG_GNUM(19)); \ +String_Format4(str, "r20=%x r21=%x r22=%x r23=%x" _NL, REG_GNUM(20), REG_GNUM(21), REG_GNUM(22), REG_GNUM(23)); \ +String_Format4(str, "r24=%x r25=%x r26=%x r27=%x" _NL, REG_GNUM(24), REG_GNUM(25), REG_GNUM(26), REG_GNUM(27)); \ +String_Format4(str, "r28=%x r29=%x r30=%x r31=%x" _NL, REG_GNUM(28), REG_GNUM(29), REG_GNUM(30), REG_GNUM(31)); + +#define Dump_MIPS() \ +String_Format4(str, "r0 =%x r1 =%x r2 =%x r3 =%x" _NL, REG_GNUM(0), REG_GNUM(1), REG_GNUM(2), REG_GNUM(3)); \ +String_Format4(str, "r4 =%x r5 =%x r6 =%x r7 =%x" _NL, REG_GNUM(4), REG_GNUM(5), REG_GNUM(6), REG_GNUM(7)); \ +String_Format4(str, "r8 =%x r9 =%x r10=%x r11=%x" _NL, REG_GNUM(8), REG_GNUM(9), REG_GNUM(10), REG_GNUM(11)); \ +String_Format4(str, "r12=%x r13=%x r14=%x r15=%x" _NL, REG_GNUM(12), REG_GNUM(13), REG_GNUM(14), REG_GNUM(15)); \ +String_Format4(str, "r16=%x r17=%x r18=%x r19=%x" _NL, REG_GNUM(16), REG_GNUM(17), REG_GNUM(18), REG_GNUM(19)); \ +String_Format4(str, "r20=%x r21=%x r22=%x r23=%x" _NL, REG_GNUM(20), REG_GNUM(21), REG_GNUM(22), REG_GNUM(23)); \ +String_Format4(str, "r24=%x r25=%x r26=%x r27=%x" _NL, REG_GNUM(24), REG_GNUM(25), REG_GNUM(26), REG_GNUM(27)); \ +String_Format4(str, "r28=%x sp =%x fp =%x ra =%x" _NL, REG_GNUM(28), REG_GNUM(29), REG_GNUM(30), REG_GNUM(31)); \ +String_Format3(str, "pc =%x lo =%x hi =%x" _NL, REG_GET_PC(), REG_GET_LO(), REG_GET_HI()); + +#if defined CC_BUILD_WIN +/* See CONTEXT in WinNT.h */ +static void PrintRegisters(cc_string* str, void* ctx) { + CONTEXT* r = (CONTEXT*)ctx; +#if defined _M_IX86 + #define REG_GET(reg, ign) &r->E ## reg + Dump_X86() +#elif defined _M_X64 + #define REG_GET(reg, ign) &r->R ## reg + Dump_X64() +#elif defined _M_ARM + #define REG_GNUM(num) &r->R ## num + #define REG_GET_FP() &r->R11 + #define REG_GET_IP() &r->R12 + #define REG_GET_SP() &r->Sp + #define REG_GET_LR() &r->Lr + #define REG_GET_PC() &r->Pc + Dump_ARM32() +#elif defined _M_ARM64 + #define REG_GNUM(num) &r->X[num] + #define REG_GET_FP() &r->Fp + #define REG_GET_LR() &r->Lr + #define REG_GET_SP() &r->Sp + #define REG_GET_PC() &r->Pc + Dump_ARM64() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_DARWIN && __DARWIN_UNIX03 +/* See /usr/include/mach/i386/_structs.h (macOS 10.5+) */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t r = ((ucontext_t*)ctx)->uc_mcontext; +#if defined __i386__ + #define REG_GET(reg, ign) &r->__ss.__e##reg + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(reg, ign) &r->__ss.__r##reg + Dump_X64() +#elif defined __aarch64__ + #define REG_GNUM(num) &r->__ss.__x[num] + #define REG_GET_FP() &r->__ss.__fp + #define REG_GET_LR() &r->__ss.__lr + #define REG_GET_SP() &r->__ss.__sp + #define REG_GET_PC() &r->__ss.__pc + Dump_ARM64() +#elif defined __arm__ + #define REG_GNUM(num) &r->__ss.__r[num] + #define REG_GET_FP() &r->__ss.__r[11] + #define REG_GET_IP() &r->__ss.__r[12] + #define REG_GET_SP() &r->__ss.__sp + #define REG_GET_LR() &r->__ss.__lr + #define REG_GET_PC() &r->__ss.__pc + Dump_ARM32() +#elif defined __ppc__ + #define REG_GNUM(num) &r->__ss.__r##num + #define REG_GET_PC() &r->__ss.__srr0 + #define REG_GET_LR() &r->__ss.__lr + #define REG_GET_CTR() &r->__ss.__ctr + Dump_PPC() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_DARWIN +/* See /usr/include/mach/i386/thread_status.h (macOS 10.4) */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t r = ((ucontext_t*)ctx)->uc_mcontext; +#if defined __i386__ + #define REG_GET(reg, ign) &r->ss.e##reg + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(reg, ign) &r->ss.r##reg + Dump_X64() +#elif defined __ppc__ + #define REG_GNUM(num) &r->ss.r##num + #define REG_GET_PC() &r->ss.srr0 + #define REG_GET_LR() &r->ss.lr + #define REG_GET_CTR() &r->ss.ctr + Dump_PPC() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_LINUX || defined CC_BUILD_ANDROID +/* See /usr/include/sys/ucontext.h */ +static void PrintRegisters(cc_string* str, void* ctx) { +#if __PPC__ && __WORDSIZE == 32 + /* See sysdeps/unix/sysv/linux/powerpc/sys/ucontext.h in glibc */ + mcontext_t r = *((ucontext_t*)ctx)->uc_mcontext.uc_regs; +#else + mcontext_t r = ((ucontext_t*)ctx)->uc_mcontext; +#endif + +#if defined __i386__ + #define REG_GET(ign, reg) &r.gregs[REG_E##reg] + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(ign, reg) &r.gregs[REG_R##reg] + Dump_X64() +#elif defined __aarch64__ + #define REG_GNUM(num) &r.regs[num] + #define REG_GET_FP() &r.regs[29] + #define REG_GET_LR() &r.regs[30] + #define REG_GET_SP() &r.sp + #define REG_GET_PC() &r.pc + Dump_ARM64() +#elif defined __arm__ + #define REG_GNUM(num) &r.arm_r##num + #define REG_GET_FP() &r.arm_fp + #define REG_GET_IP() &r.arm_ip + #define REG_GET_SP() &r.arm_sp + #define REG_GET_LR() &r.arm_lr + #define REG_GET_PC() &r.arm_pc + Dump_ARM32() +#elif defined __alpha__ + #define REG_GNUM(num) &r.sc_regs[num] + #define REG_GET_FP() &r.sc_regs[15] + #define REG_GET_PC() &r.sc_pc + #define REG_GET_SP() &r.sc_regs[30] + Dump_Alpha() +#elif defined __sparc__ + #define REG_GET(ign, reg) &r.gregs[REG_##reg] + Dump_SPARC() +#elif defined __PPC__ && __WORDSIZE == 32 + #define REG_GNUM(num) &r.gregs[num] + #define REG_GET_PC() &r.gregs[32] + #define REG_GET_LR() &r.gregs[35] + #define REG_GET_CTR() &r.gregs[34] + Dump_PPC() +#elif defined __PPC__ + #define REG_GNUM(num) &r.gp_regs[num] + #define REG_GET_PC() &r.gp_regs[32] + /* TODO this might be wrong, compare with PT_LNK in */ + /* https://elixir.bootlin.com/linux/v4.19.122/source/arch/powerpc/include/uapi/asm/ptrace.h#L102 */ + #define REG_GET_LR() &r.gp_regs[35] + #define REG_GET_CTR() &r.gp_regs[34] + Dump_PPC() +#elif defined __mips__ + #define REG_GNUM(num) &r.gregs[num] + #define REG_GET_PC() &r.pc + #define REG_GET_LO() &r.mdlo + #define REG_GET_HI() &r.mdhi + Dump_MIPS() +#elif defined __riscv + #define REG_GNUM(num) &r.__gregs[num] + #define REG_GET_PC() &r.__gregs[REG_PC] + Dump_RISCV() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_SOLARIS +/* See /usr/include/sys/regset.h */ +/* -> usr/src/uts/[ARCH]/sys/mcontext.h */ +/* -> usr/src/uts/[ARCH]/sys/regset.h */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t* r = &((ucontext_t*)ctx)->uc_mcontext; + +#if defined __i386__ + #define REG_GET(ign, reg) &r->gregs[E##reg] + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(ign, reg) &r->gregs[REG_R##reg] + Dump_X64() +#elif defined __sparc__ + #define REG_GET(ign, reg) &r->gregs[REG_##reg] + Dump_SPARC() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_NETBSD +/* See /usr/include/[ARCH]/mcontext.h */ +/* -> src/sys/arch/[ARCH]/include/mcontext.h */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t* r = &((ucontext_t*)ctx)->uc_mcontext; +#if defined __i386__ + #define REG_GET(ign, reg) &r->__gregs[_REG_E##reg] + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(ign, reg) &r->__gregs[_REG_R##reg] + Dump_X64() +#elif defined __aarch64__ + #define REG_GNUM(num) &r->__gregs[num] + #define REG_GET_FP() &r->__gregs[_REG_FP] + #define REG_GET_LR() &r->__gregs[_REG_LR] + #define REG_GET_SP() &r->__gregs[_REG_SP] + #define REG_GET_PC() &r->__gregs[_REG_PC] + Dump_ARM64() +#elif defined __arm__ + #define REG_GNUM(num) &r->__gregs[num] + #define REG_GET_FP() &r->__gregs[_REG_FP] + #define REG_GET_IP() &r->__gregs[12] + #define REG_GET_SP() &r->__gregs[_REG_SP] + #define REG_GET_LR() &r->__gregs[_REG_LR] + #define REG_GET_PC() &r->__gregs[_REG_PC] + Dump_ARM32() +#elif defined __powerpc__ + #define REG_GNUM(num) &r->__gregs[num] + #define REG_GET_PC() &r->__gregs[_REG_PC] + #define REG_GET_LR() &r->__gregs[_REG_LR] + #define REG_GET_CTR() &r->__gregs[_REG_CTR] + Dump_PPC() +#elif defined __mips__ + #define REG_GNUM(num) &r->__gregs[num] + #define REG_GET_PC() &r->__gregs[_REG_EPC] + #define REG_GET_LO() &r->__gregs[_REG_MDLO] + #define REG_GET_HI() &r->__gregs[_REG_MDHI] + Dump_MIPS() +#elif defined __riscv + #define REG_GNUM(num) &r->__gregs[num] + #define REG_GET_PC() &r->__gregs[_REG_PC] + Dump_RISCV() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_FREEBSD +/* See /usr/include/machine/ucontext.h */ +/* -> src/sys/[ARCH]/include/ucontext.h */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t* r = &((ucontext_t*)ctx)->uc_mcontext; +#if defined __i386__ + #define REG_GET(reg, ign) &r->mc_e##reg + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(reg, ign) &r->mc_r##reg + Dump_X64() +#elif defined __aarch64__ + #define REG_GNUM(num) &r->mc_gpregs.gp_x[num] + #define REG_GET_FP() &r->mc_gpregs.gp_x[29] + #define REG_GET_LR() &r->mc_gpregs.gp_lr + #define REG_GET_SP() &r->mc_gpregs.gp_sp + #define REG_GET_PC() &r->mc_gpregs.gp_elr + Dump_ARM64() +#elif defined __arm__ + #define REG_GNUM(num) &r->__gregs[num] + #define REG_GET_FP() &r->__gregs[_REG_FP] + #define REG_GET_IP() &r->__gregs[12] + #define REG_GET_SP() &r->__gregs[_REG_SP] + #define REG_GET_LR() &r->__gregs[_REG_LR] + #define REG_GET_PC() &r->__gregs[_REG_PC] + Dump_ARM32() +#elif defined __powerpc__ + #define REG_GNUM(num) &r->mc_frame[##num] + #define REG_GET_PC() &r->mc_srr0 + #define REG_GET_LR() &r->mc_lr + #define REG_GET_CTR() &r->mc_ctr + Dump_PPC() +#elif defined __mips__ + #define REG_GNUM(num) &r->mc_regs[num] + #define REG_GET_PC() &r->mc_pc + #define REG_GET_LO() &r->mullo + #define REG_GET_HI() &r->mulhi + Dump_MIPS() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_OPENBSD +/* See /usr/include/machine/signal.h */ +/* -> src/sys/arch/[ARCH]/include/signal.h */ +static void PrintRegisters(cc_string* str, void* ctx) { + ucontext_t* r = (ucontext_t*)ctx; +#if defined __i386__ + #define REG_GET(reg, ign) &r->sc_e##reg + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(reg, ign) &r->sc_r##reg + Dump_X64() +#elif defined __aarch64__ + #define REG_GNUM(num) &r->sc_x[num] + #define REG_GET_FP() &r->sc_x[29] + #define REG_GET_LR() &r->sc_lr + #define REG_GET_SP() &r->sc_sp + #define REG_GET_PC() &r->sc_elr + Dump_ARM64() +#elif defined __arm__ + #define REG_GNUM(num) &r->sc_r##num + #define REG_GET_FP() &r->sc_r11 + #define REG_GET_IP() &r->sc_r12 + #define REG_GET_SP() &r->sc_usr_sp + #define REG_GET_LR() &r->sc_usr_lr + #define REG_GET_PC() &r->sc_pc + Dump_ARM32() +#elif defined __powerpc__ + #define REG_GNUM(num) &r->sc_frame.fixreg[num] + #define REG_GET_PC() &r->sc_frame.srr0 + #define REG_GET_LR() &r->sc_frame.lr + #define REG_GET_CTR() &r->sc_frame.ctr + Dump_PPC() +#elif defined __mips__ + #define REG_GNUM(num) &r->sc_regs[num] + #define REG_GET_PC() &r->sc_pc + #define REG_GET_LO() &r->mullo + #define REG_GET_HI() &r->mulhi + Dump_MIPS() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_HAIKU +/* See /headers/posix/arch/[ARCH]/signal.h */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t* r = &((ucontext_t*)ctx)->uc_mcontext; +#if defined __i386__ + #define REG_GET(reg, ign) &r->e##reg + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(reg, ign) &r->r##reg + Dump_X64() +#elif defined __aarch64__ + #define REG_GNUM(num) &r->x[num] + #define REG_GET_FP() &r->x[29] + #define REG_GET_LR() &r->lr + #define REG_GET_SP() &r->sp + #define REG_GET_PC() &r->elr + Dump_ARM64() +#elif defined __arm__ + #define REG_GNUM(num) &r->r##num + #define REG_GET_FP() &r->r11 + #define REG_GET_IP() &r->r12 + #define REG_GET_SP() &r->r13 + #define REG_GET_LR() &r->r14 + #define REG_GET_PC() &r->r15 + Dump_ARM32() +#elif defined __riscv + #define REG_GNUM(num) &r->x[num - 1] + #define REG_GET_PC() &r->pc + Dump_RISCV() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_SERENITY +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t* r = &((ucontext_t*)ctx)->uc_mcontext; +#if defined __i386__ + #define REG_GET(reg, ign) &r->e##reg + Dump_X86() +#elif defined __x86_64__ + #define REG_GET(reg, ign) &r->r##reg + Dump_X64() +#else + #error "Unknown CPU architecture" +#endif +} +#elif defined CC_BUILD_IRIX +/* See /usr/include/sys/ucontext.h */ +/* https://nixdoc.net/man-pages/IRIX/man5/UCONTEXT.5.html */ +static void PrintRegisters(cc_string* str, void* ctx) { + mcontext_t* r = &((ucontext_t*)ctx)->uc_mcontext; + + #define REG_GNUM(num) &r->__gregs[CTX_EPC] + #define REG_GET_PC() &r->__gregs[CTX_MDLO] + #define REG_GET_LO() &r->__gregs[CTX_MDLO] + #define REG_GET_HI() &r->__gregs[CTX_MDHI] + Dump_MIPS() +} +#else +static void PrintRegisters(cc_string* str, void* ctx) { + /* Register dumping not implemented */ +} +#endif + +static void DumpRegisters(void* ctx) { + cc_string str; char strBuffer[768]; + String_InitArray(str, strBuffer); + + String_AppendConst(&str, "-- registers --" _NL); + PrintRegisters(&str, ctx); + Logger_Log(&str); +} + + +/*########################################################################################################################* +*------------------------------------------------Module/Memory map handling-----------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_WIN +static void DumpStack(void) { + static const cc_string stack = String_FromConst("-- stack --\r\n"); + cc_string str; char strBuffer[128]; + cc_uint8 buffer[0x10]; + SIZE_T numRead; + int i, j; + + Logger_Log(&stack); + spRegister &= ~0x0F; + spRegister -= 0x40; + + /* Dump 128 bytes near stack pointer */ + for (i = 0; i < 8; i++, spRegister += 0x10) + { + String_InitArray(str, strBuffer); + String_Format1(&str, "0x%x)", &spRegister); + ReadProcessMemory(CUR_PROCESS_HANDLE, (LPCVOID)spRegister, buffer, 0x10, &numRead); + + for (j = 0; j < 0x10; j++) + { + if ((j & 0x03) == 0) String_Append(&str, ' '); + String_AppendHex(&str, buffer[j]); + String_Append(&str, ' '); + } + String_AppendConst(&str, "\r\n"); + Logger_Log(&str); + } +} + +static BOOL CALLBACK DumpModule(const char* name, ULONG_PTR base, ULONG size, void* userCtx) { + cc_string str; char strBuffer[256]; + cc_uintptr beg, end; + + beg = base; end = base + (size - 1); + String_InitArray(str, strBuffer); + + String_Format3(&str, "%c = %x-%x\r\n", name, &beg, &end); + Logger_Log(&str); + return true; +} + +static BOOL (WINAPI *_EnumerateLoadedModules)(HANDLE process, PENUMLOADED_MODULES_CALLBACK callback, PVOID userContext); +static void DumpMisc(void) { + static const cc_string modules = String_FromConst("-- modules --\r\n"); + if (spRegister >= 0xFFFF) DumpStack(); + + if (!_EnumerateLoadedModules) return; + Logger_Log(&modules); + _EnumerateLoadedModules(curProcess, DumpModule, NULL); +} + +#elif defined CC_BUILD_LINUX || defined CC_BUILD_SOLARIS || defined CC_BUILD_ANDROID +#include +#include +#include + +#ifdef CC_BUILD_ANDROID +static int SkipRange(const cc_string* str) { + /* Android has a lot of ranges in /maps, which produces 100-120 kb of logs for one single crash! */ + /* Example of different types of ranges: + 7a2df000-7a2eb000 r-xp 00000000 fd:01 419 /vendor/lib/libdrm.so + 7a2eb000-7a2ec000 r--p 0000b000 fd:01 419 /vendor/lib/libdrm.so + 7a2ec000-7a2ed000 rw-p 0000c000 fd:01 419 /vendor/lib/libdrm.so + 7a3d5000-7a4d1000 rw-p 00000000 00:00 0 + 7a4d1000-7a4d2000 ---p 00000000 00:00 0 [anon:thread stack guard] + To cut down crash logs to more relevant information, unnecessary '/' entries are ignored */ + cc_string path; + + /* Always include ranges without a / */ + int idx = String_IndexOf(str, '/'); + if (idx == -1) return false; + path = String_UNSAFE_SubstringAt(str, idx); + + return + /* Ignore fonts */ + String_ContainsConst(&path, "/system/fonts/") + /* Ignore shared memory (e.g. '/dev/ashmem/dalvik-thread local mark stack (deleted)') */ + || String_ContainsConst(&path, "/dev/ashmem/") + /* Ignore /dev/mali0 ranges (~200 entries on some devices) */ + || String_ContainsConst(&path, "/dev/mali0") + /* Ignore /system/lib/ (~350 entries) and /system/lib64 (~700 entries) */ + || String_ContainsConst(&path, "/system/lib") + /* Ignore /system/framework (~130 to ~160 entries) */ + || String_ContainsConst(&path, "/system/framework/") + /* Ignore /apex/com.android.art/javalib/ (~240 entries) */ + || String_ContainsConst(&path, "/apex/com.") + /* Ignore /dev/dri/renderD128 entries (~100 to ~120 entries) */ + || String_ContainsConst(&path, "/dri/renderD128") + /* Ignore /data/dalvik-cache entries (~80 entries) */ + || String_ContainsConst(&path, "/data/dalvik-cache/") + /* Ignore /vendor/lib entries (~80 entries) */ + || String_ContainsConst(&path, "/vendor/lib"); +} +#else +static int SkipRange(const cc_string* str) { + return + /* Ignore GPU iris driver i915 GEM buffers (~60,000 entries for one user) */ + String_ContainsConst(str, "anon_inode:i915.gem"); +} +#endif + +static void DumpMisc(void) { + static const cc_string memMap = String_FromConst("-- memory map --\n"); + cc_string str; char strBuffer[320]; + int n, fd; + + Logger_Log(&memMap); + /* dumps all known ranges of memory */ + fd = open("/proc/self/maps", O_RDONLY); + if (fd < 0) return; + String_InitArray(str, strBuffer); + + while ((n = read(fd, str.buffer, str.capacity)) > 0) { + str.length = n; + if (!SkipRange(&str)) Logger_Log(&str); + } + + close(fd); +} +#elif defined CC_BUILD_DARWIN +#include + +static void DumpMisc(void) { + static const cc_string modules = String_FromConst("-- modules --\n"); + static const cc_string newLine = String_FromConst(_NL); + cc_uint32 i, count; + const char* path; + cc_string str; + + /* TODO: Add Logger_LogRaw / Logger_LogConst too */ + Logger_Log(&modules); + count = _dyld_image_count(); + /* dumps absolute path of each module */ + + for (i = 0; i < count; i++) { + path = _dyld_get_image_name(i); + if (!path) continue; + + str = String_FromReadonly(path); + Logger_Log(&str); + Logger_Log(&newLine); + } +} +#else +static void DumpMisc(void) { } +#endif + + +/*########################################################################################################################* +*--------------------------------------------------Unhandled error logging------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_WIN +static const char* ExceptionDescribe(cc_uint32 code) { + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: return "ACCESS_VIOLATION"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "ILLEGAL_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "DIVIDE_BY_ZERO"; + } + return NULL; +} + +static LONG WINAPI UnhandledFilter(struct _EXCEPTION_POINTERS* info) { + cc_string msg; char msgBuffer[128 + 1]; + const char* desc; + cc_uint32 code; + cc_uintptr addr; + DWORD i, numArgs; + + code = (cc_uint32)info->ExceptionRecord->ExceptionCode; + addr = (cc_uintptr)info->ExceptionRecord->ExceptionAddress; + desc = ExceptionDescribe(code); + + String_InitArray_NT(msg, msgBuffer); + if (desc) { + String_Format2(&msg, "Unhandled %c error at %x", desc, &addr); + } else { + String_Format2(&msg, "Unhandled exception 0x%h at %x", &code, &addr); + } + + numArgs = info->ExceptionRecord->NumberParameters; + if (numArgs) { + numArgs = min(numArgs, EXCEPTION_MAXIMUM_PARAMETERS); + String_AppendConst(&msg, " ["); + + for (i = 0; i < numArgs; i++) { + String_Format1(&msg, "0x%x,", &info->ExceptionRecord->ExceptionInformation[i]); + } + String_Append(&msg, ']'); + } + + msg.buffer[msg.length] = '\0'; + AbortCommon(0, msg.buffer, info->ContextRecord); + return EXCEPTION_EXECUTE_HANDLER; /* TODO: different flag */ +} + +void Logger_Hook(void) { + static const struct DynamicLibSym funcs[] = { + #ifdef _IMAGEHLP64 + { "EnumerateLoadedModules64", (void**)&_EnumerateLoadedModules}, + #else + { "EnumerateLoadedModules", (void**)&_EnumerateLoadedModules }, + #endif + }; + static const cc_string imagehlp = String_FromConst("IMAGEHLP.DLL"); + OSVERSIONINFOA osInfo; + void* lib; + + SetUnhandledExceptionFilter(UnhandledFilter); + DynamicLib_LoadAll(&imagehlp, funcs, Array_Elems(funcs), &lib); + + /* Windows 9x requires process IDs instead - see old DBGHELP docs */ + /* https://documentation.help/DbgHelp/documentation.pdf */ + osInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA); + osInfo.dwPlatformId = 0; + GetVersionExA(&osInfo); + + if (osInfo.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) { + curProcess = (HANDLE)((cc_uintptr)GetCurrentProcessId()); + } +} +#elif defined CC_BUILD_POSIX +static const char* SignalDescribe(int type) { + switch (type) { + case SIGSEGV: return "SIGSEGV"; + case SIGBUS: return "SIGBUS"; + case SIGILL: return "SIGILL"; + case SIGABRT: return "SIGABRT"; + case SIGFPE: return "SIGFPE"; + } + return NULL; +} + +static void SignalHandler(int sig, siginfo_t* info, void* ctx) { + cc_string msg; char msgBuffer[128 + 1]; + const char* desc; + int type, code; + cc_uintptr addr; + + /* Uninstall handler to avoid chance of infinite loop */ + signal(SIGSEGV, SIG_DFL); + signal(SIGBUS, SIG_DFL); + signal(SIGILL, SIG_DFL); + signal(SIGABRT, SIG_DFL); + signal(SIGFPE, SIG_DFL); + + type = info->si_signo; + code = info->si_code; + addr = (cc_uintptr)info->si_addr; + desc = SignalDescribe(type); + + String_InitArray_NT(msg, msgBuffer); + if (desc) { + String_Format3(&msg, "Unhandled signal %c (code %i) at %x", desc, &code, &addr); + } else { + String_Format3(&msg, "Unhandled signal %i (code %i) at %x", &type, &code, &addr); + } + msg.buffer[msg.length] = '\0'; + + #if defined CC_BUILD_ANDROID + /* deliberate Dalvik VM abort, try to log a nicer error for this */ + if (type == SIGSEGV && addr == 0xDEADD00D) Platform_TryLogJavaError(); + #endif + AbortCommon(0, msg.buffer, ctx); +} + +void Logger_Hook(void) { + struct sigaction sa, old; + sa.sa_sigaction = SignalHandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART | SA_SIGINFO; + + sigaction(SIGSEGV, &sa, &old); + sigaction(SIGBUS, &sa, &old); + sigaction(SIGILL, &sa, &old); + sigaction(SIGABRT, &sa, &old); + sigaction(SIGFPE, &sa, &old); +} +#else +void Logger_Hook(void) { + /* TODO can signals be supported somehow for PSP/3DS? */ +} +#endif + + +/*########################################################################################################################* +*-------------------------------------------------Deliberate crash logging------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_WIN +#if __GNUC__ +/* Don't want compiler doing anything fancy with registers */ +void __attribute__((optimize("O0"))) Logger_Abort2(cc_result result, const char* raw_msg) { +#else +void Logger_Abort2(cc_result result, const char* raw_msg) { +#endif + CONTEXT ctx; + #if _M_IX86 && __GNUC__ + /* Stack frame layout on x86: */ + /* [ebp] is previous frame's EBP */ + /* [ebp+4] is previous frame's EIP (return address) */ + /* address of [ebp+8] is previous frame's ESP */ + __asm__( + "mov 0(%%ebp), %%eax \n\t" /* mov eax, [ebp] */ + "mov %%eax, %0 \n\t" /* mov [ctx.Ebp], eax */ + "mov 4(%%ebp), %%eax \n\t" /* mov eax, [ebp+4] */ + "mov %%eax, %1 \n\t" /* mov [ctx.Eip], eax */ + "lea 8(%%ebp), %%eax \n\t" /* lea eax, [ebp+8] */ + "mov %%eax, %2" /* mov [ctx.Esp], eax */ + : "=m" (ctx.Ebp), "=m" (ctx.Eip), "=m" (ctx.Esp) + : + : "eax", "memory" + ); + ctx.ContextFlags = CONTEXT_CONTROL; + #else + /* This method is guaranteed to exist on 64 bit windows. */ + /* NOTE: This is missing in 32 bit Windows 2000 however, */ + /* so an alternative is provided for MinGW above so that */ + /* the game can be cross-compiled for Windows 98 / 2000 */ + RtlCaptureContext(&ctx); + #endif + AbortCommon(result, raw_msg, &ctx); +} +#else +void Logger_Abort2(cc_result result, const char* raw_msg) { + AbortCommon(result, raw_msg, NULL); +} +#endif + + +/*########################################################################################################################* +*----------------------------------------------------------Common---------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_MINFILES +void Logger_Log(const cc_string* msg) { + Platform_Log(msg->buffer, msg->length); +} +static void LogCrashHeader(void) { } +static void CloseLogFile(void) { } +#else +static struct Stream logStream; +static cc_bool logOpen; + +void Logger_Log(const cc_string* msg) { + static const cc_string path = String_FromConst("client.log"); + if (!logOpen) { + logOpen = true; + Stream_AppendFile(&logStream, &path); + } + + if (!logStream.meta.file) return; + Stream_Write(&logStream, (const cc_uint8*)msg->buffer, msg->length); +} + +static void LogCrashHeader(void) { + cc_string msg; char msgBuffer[96]; + struct DateTime now; + + String_InitArray(msg, msgBuffer); + String_AppendConst(&msg, _NL "----------------------------------------" _NL); + Logger_Log(&msg); + msg.length = 0; + + DateTime_CurrentLocal(&now); + String_Format3(&msg, "Crash time: %p2/%p2/%p4 ", &now.day, &now.month, &now.year); + String_Format3(&msg, "%p2:%p2:%p2" _NL, &now.hour, &now.minute, &now.second); + Logger_Log(&msg); +} + +static void CloseLogFile(void) { + if (logStream.meta.file) logStream.Close(&logStream); +} +#endif + +#if CC_GFX_BACKEND == CC_GFX_BACKEND_D3D11 + #define GFX_BACKEND " (Direct3D11)" +#elif CC_GFX_BACKEND == CC_GFX_BACKEND_D3D9 + #define GFX_BACKEND " (Direct3D9)" +#elif CC_GFX_BACKEND == CC_GFX_BACKEND_GL2 + #define GFX_BACKEND " (ModernGL)" +#elif CC_GFX_BACKEND == CC_GFX_BACKEND_GL1 + #define GFX_BACKEND " (OpenGL)" +#else + #define GFX_BACKEND " (Unknown)" +#endif + +static void AbortCommon(cc_result result, const char* raw_msg, void* ctx) { + static const cc_string backtrace = String_FromConst("-- backtrace --" _NL); + cc_string msg; char msgBuffer[3070 + 1]; + String_InitArray_NT(msg, msgBuffer); + + String_AppendConst(&msg, "ClassiCube crashed." _NL); + if (raw_msg) String_Format1(&msg, "Reason: %c" _NL, raw_msg); + #ifdef CC_COMMIT_SHA + String_AppendConst(&msg, "Commit SHA: " CC_COMMIT_SHA GFX_BACKEND _NL); + #endif + + if (result) { + String_Format1(&msg, "%h" _NL, &result); + } else { result = 1; } + + LogCrashHeader(); + Logger_Log(&msg); + + String_AppendConst(&msg, "Full details of the crash have been logged to 'client.log'.\n"); + String_AppendConst(&msg, "Please report this on the ClassiCube forums or to UnknownShadow200.\n\n"); + if (ctx) DumpRegisters(ctx); + + /* These two function calls used to be in a separate DumpBacktrace function */ + /* However that was not always inlined by the compiler, which resulted in a */ + /* useless strackframe being logged - so manually inline Logger_Backtrace call */ + Logger_Log(&backtrace); + Logger_Backtrace(&msg, ctx); + + DumpMisc(); + CloseLogFile(); + + msg.buffer[msg.length] = '\0'; + Window_ShowDialog("We're sorry", msg.buffer); + Process_Exit(result); +} + +void Logger_Abort(const char* raw_msg) { Logger_Abort2(0, raw_msg); } + +void Logger_FailToStart(const char* raw_msg) { + cc_string msg = String_FromReadonly(raw_msg); + + Window_ShowDialog("Failed to start ClassiCube", raw_msg); + LogCrashHeader(); + Logger_Log(&msg); + Process_Exit(1); +} + + +#if defined CC_BACKTRACE_UNWIND +#include + +static _Unwind_Reason_Code UnwindFrame(struct _Unwind_Context* ctx, void* arg) { + cc_uintptr addr = _Unwind_GetIP(ctx); + if (!addr) return _URC_END_OF_STACK; + + DumpFrame((cc_string*)arg, (void*)addr); + return _URC_NO_REASON; +} + +void Logger_Backtrace(cc_string* trace, void* ctx) { + _Unwind_Backtrace(UnwindFrame, trace); + String_AppendConst(trace, _NL); +} +#endif + +#if defined CC_BACKTRACE_BUILTIN +static CC_NOINLINE void* GetReturnAddress(int level) { + /* "... a value of 0 yields the return address of the current function, a value of 1 yields the return address of the caller of the current function" */ + switch(level) { + case 0: return __builtin_return_address(1); + case 1: return __builtin_return_address(2); + case 2: return __builtin_return_address(3); + case 3: return __builtin_return_address(4); + case 4: return __builtin_return_address(5); + case 5: return __builtin_return_address(6); + case 6: return __builtin_return_address(7); + case 7: return __builtin_return_address(8); + case 8: return __builtin_return_address(9); + case 9: return __builtin_return_address(10); + case 10: return __builtin_return_address(11); + case 11: return __builtin_return_address(12); + case 12: return __builtin_return_address(13); + case 13: return __builtin_return_address(14); + case 14: return __builtin_return_address(15); + case 15: return __builtin_return_address(16); + case 16: return __builtin_return_address(17); + case 17: return __builtin_return_address(18); + case 18: return __builtin_return_address(19); + case 19: return __builtin_return_address(20); + case 20: return __builtin_return_address(21); + default: return NULL; + } +} + +static CC_NOINLINE void* GetFrameAddress(int level) { + /* "... a value of 0 yields the frame address of the current function, a value of 1 yields the frame address of the caller of the current function, and so forth." */ + switch(level) { + case 0: return __builtin_frame_address(1); + case 1: return __builtin_frame_address(2); + case 2: return __builtin_frame_address(3); + case 3: return __builtin_frame_address(4); + case 4: return __builtin_frame_address(5); + case 5: return __builtin_frame_address(6); + case 6: return __builtin_frame_address(7); + case 7: return __builtin_frame_address(8); + case 8: return __builtin_frame_address(9); + case 9: return __builtin_frame_address(10); + case 10: return __builtin_frame_address(11); + case 11: return __builtin_frame_address(12); + case 12: return __builtin_frame_address(13); + case 13: return __builtin_frame_address(14); + case 14: return __builtin_frame_address(15); + case 15: return __builtin_frame_address(16); + case 16: return __builtin_frame_address(17); + case 17: return __builtin_frame_address(18); + case 18: return __builtin_frame_address(19); + case 19: return __builtin_frame_address(20); + case 20: return __builtin_frame_address(21); + default: return NULL; + } +} + +void Logger_Backtrace(cc_string* trace, void* ctx) { + void* addrs[MAX_BACKTRACE_FRAMES]; + int i, frames; + + /* See https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html */ + /* Note "Calling this function with a nonzero argument can have unpredictable effects, including crashing the calling program" */ + /* So this probably only works on x86/x86_64 */ + for (i = 0; GetFrameAddress(i + 1) && i < MAX_BACKTRACE_FRAMES; i++) + { + addrs[i] = GetReturnAddress(i); + if (!addrs[i]) break; + } + + frames = i; + for (i = 0; i < frames; i++) { + DumpFrame(trace, addrs[i]); + } + String_AppendConst(trace, _NL); +} +#endif diff --git a/src/Logger.h b/src/Logger.h new file mode 100644 index 0000000..cea1b84 --- /dev/null +++ b/src/Logger.h @@ -0,0 +1,58 @@ +#ifndef CC_LOGGER_H +#define CC_LOGGER_H +#include "Core.h" +/* +Logs warnings/errors and also abstracts platform specific logging for fatal errors +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +typedef cc_bool (*Logger_DescribeError)(cc_result res, cc_string* dst); +typedef void (*Logger_DoWarn)(const cc_string* msg); +/* Informs the user about a non-fatal error. */ +/* By default this shows a message box, but changes to in-game chat when game is running. */ +extern Logger_DoWarn Logger_WarnFunc; +/* The title shown for warning dialogs. */ +extern const char* Logger_DialogTitle; +/* Shows a warning message box with the given message. */ +void Logger_DialogWarn(const cc_string* msg); + +/* Format: "Error [result] when [action] \n Error meaning: [desc]" */ +/* If describeErr returns false, then 'Error meaning' line is omitted. */ +void Logger_FormatWarn(cc_string* msg, cc_result res, const char* action, Logger_DescribeError describeErr); +/* Format: "Error [result] when [action] '[path]' \n Error meaning: [desc]" */ +/* If describeErr returns false, then 'Error meaning' line is omitted. */ +void Logger_FormatWarn2(cc_string* msg, cc_result res, const char* action, const cc_string* path, Logger_DescribeError describeErr); +/* Informs the user about a non-fatal error, with an optional meaning message. */ +void Logger_Warn(cc_result res, const char* action, Logger_DescribeError describeErr); +/* Informs the user about a non-fatal error, with an optional meaning message. */ +void Logger_Warn2(cc_result res, const char* action, const cc_string* path, Logger_DescribeError describeErr); + +/* Shortcut for Logger_Warn with no error describer function */ +void Logger_SimpleWarn(cc_result res, const char* action); +/* Shorthand for Logger_Warn2 with no error describer function */ +void Logger_SimpleWarn2(cc_result res, const char* action, const cc_string* path); +/* Shows a warning for a failed DynamicLib_Load2/Get2 call. */ +/* Format: "Error [action] '[path]' \n Error meaning: [desc]" */ +void Logger_DynamicLibWarn(const char* action, const cc_string* path); +/* Shortcut for Logger_Warn with Platform_DescribeError */ +void Logger_SysWarn(cc_result res, const char* action); +/* Shortcut for Logger_Warn2 with Platform_DescribeError */ +void Logger_SysWarn2(cc_result res, const char* action, const cc_string* path); + +/* Hooks the operating system's unhandled error callback/signal. */ +/* This is used to attempt to log some information about a crash due to invalid memory read, etc. */ +void Logger_Hook(void); +/* Generates a backtrace based on the platform-specific CPU context. */ +/* NOTE: The provided CPU context may be modified (e.g on Windows) */ +void Logger_Backtrace(cc_string* trace, void* ctx); +/* Logs a message to client.log on disc. */ +/* NOTE: The message is written raw, it is NOT converted to unicode (unlike Stream_WriteLine) */ +void Logger_Log(const cc_string* msg); +/* Displays a message box with raw_msg body, logs state to disc, then immediately terminates/quits. */ +/* Typically used to abort due to an unrecoverable error. (e.g. out of memory) */ +void Logger_Abort(const char* raw_msg); +/* Displays a message box with raw_msg body, logs state to disc, then immediately terminates/quits. */ +/* Typically used to abort due to an unrecoverable error. (e.g. out of memory) */ +CC_NOINLINE void Logger_Abort2(cc_result result, const char* raw_msg); +void Logger_FailToStart(const char* raw_msg); +#endif diff --git a/src/MapRenderer.c b/src/MapRenderer.c new file mode 100644 index 0000000..d09e7d2 --- /dev/null +++ b/src/MapRenderer.c @@ -0,0 +1,809 @@ +#include "MapRenderer.h" +#include "Block.h" +#include "Builder.h" +#include "Camera.h" +#include "Entity.h" +#include "EnvRenderer.h" +#include "Event.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Game.h" +#include "Graphics.h" +#include "Platform.h" +#include "TexturePack.h" +#include "Utils.h" +#include "World.h" +#include "Options.h" + +int MapRenderer_1DUsedCount; +struct ChunkPartInfo* MapRenderer_PartsNormal; +struct ChunkPartInfo* MapRenderer_PartsTranslucent; + +static cc_bool inTranslucent; +static IVec3 chunkPos; + +/* The number of non-empty Normal/Translucent ChunkPartInfos (across entire world) for each 1D atlas batch. */ +/* 1D atlas batches that do not have any ChunkPartInfos can be entirely skipped. */ +static int normPartsCount[ATLAS1D_MAX_ATLASES], tranPartsCount[ATLAS1D_MAX_ATLASES]; +/* Whether there are any visible Normal/Translucent ChunkPartInfos for each 1D atlas batch. */ +/* 1D atlas batches that do not have any visible ChunkPartInfos can be skipped. */ +static cc_bool hasNormParts[ATLAS1D_MAX_ATLASES], hasTranParts[ATLAS1D_MAX_ATLASES]; +/* Whether renderer should check if there are any visible Normal/Translucent ChunkPartInfos for each 1D atlas batch. */ +static cc_bool checkNormParts[ATLAS1D_MAX_ATLASES], checkTranParts[ATLAS1D_MAX_ATLASES]; + +/* Render info for all chunks in the world. Unsorted. */ +static struct ChunkInfo* mapChunks; +/* Pointers to render info for all chunks in the world, sorted by distance from the camera. */ +static struct ChunkInfo** sortedChunks; +/* Pointers to render info for all chunks in the world, sorted by distance from the camera. */ +/* Only chunks that can be rendered (i.e. not empty and are visible) are included in this. */ +static struct ChunkInfo** renderChunks; +/* Number of actually used pointers in the renderChunks array. Entries past this are ignored and skipped. */ +static int renderChunksCount; +/* Distance of each chunk from the camera. */ +static cc_uint32* distances; +/* Maximum number of chunk updates that can be performed in one frame. */ +static int maxChunkUpdates; +/* Cached number of chunks in the world */ +static int chunksCount; + +static void ChunkInfo_Reset(struct ChunkInfo* chunk, int x, int y, int z) { + chunk->centreX = x + HALF_CHUNK_SIZE; chunk->centreY = y + HALF_CHUNK_SIZE; + chunk->centreZ = z + HALF_CHUNK_SIZE; +#ifndef CC_BUILD_GL11 + chunk->vb = 0; +#endif + + chunk->visible = true; + chunk->empty = false; + chunk->dirty = false; + chunk->allAir = false; + chunk->noData = true; + + chunk->drawXMin = false; chunk->drawXMax = false; chunk->drawZMin = false; + chunk->drawZMax = false; chunk->drawYMin = false; chunk->drawYMax = false; + + chunk->normalParts = NULL; + chunk->translucentParts = NULL; +} + +/* Index of maximum used 1D atlas + 1 */ +CC_NOINLINE static int MapRenderer_UsedAtlases(void) { + TextureLoc maxLoc = 0; + int i; + + for (i = 0; i < Array_Elems(Blocks.Textures); i++) { + maxLoc = max(maxLoc, Blocks.Textures[i]); + } + return Atlas1D_Index(maxLoc) + 1; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Map rendering-----------------------------------------------------* +*#########################################################################################################################*/ +static void CheckWeather(float delta) { + IVec3 pos; + BlockID block; + cc_bool outside; + IVec3_Floor(&pos, &Camera.CurrentPos); + + block = World_SafeGetBlock(pos.x, pos.y, pos.z); + outside = pos.y < 0 || !World_ContainsXZ(pos.x, pos.z); + inTranslucent = Blocks.Draw[block] == DRAW_TRANSLUCENT || (pos.y < Env.EdgeHeight && outside); + + /* If we are under water, render weather before to blend properly */ + if (!inTranslucent || Env.Weather == WEATHER_SUNNY) return; + Gfx_SetAlphaBlending(true); + EnvRenderer_RenderWeather(delta); + Gfx_SetAlphaBlending(false); +} + +#ifdef CC_BUILD_GL11 +#define DrawFace(face, ign) Gfx_BindVb(part.vbs[face]); Gfx_DrawIndexedTris_T2fC4b(0, 0); +#define DrawFaces(f1, f2, ign) DrawFace(f1, ign); DrawFace(f2, ign); +#else +#define DrawFace(face, offset) Gfx_DrawIndexedTris_T2fC4b(part.counts[face], offset); +#define DrawFaces(f1, f2, offset) Gfx_DrawIndexedTris_T2fC4b(part.counts[f1] + part.counts[f2], offset); +#endif + +#define DrawNormalFaces(minFace, maxFace) \ +if (drawMin && drawMax) { \ + Gfx_SetFaceCulling(true); \ + DrawFaces(minFace, maxFace, offset); \ + Gfx_SetFaceCulling(false); \ + Game_Vertices += (part.counts[minFace] + part.counts[maxFace]); \ +} else if (drawMin) { \ + DrawFace(minFace, offset); \ + Game_Vertices += part.counts[minFace]; \ +} else if (drawMax) { \ + DrawFace(maxFace, offset + part.counts[minFace]); \ + Game_Vertices += part.counts[maxFace]; \ +} + +static void RenderNormalBatch(int batch) { + int batchOffset = chunksCount * batch; + struct ChunkInfo* info; + struct ChunkPartInfo part; + cc_bool drawMin, drawMax; + int i, offset, count; + + for (i = 0; i < renderChunksCount; i++) { + info = renderChunks[i]; + if (!info->normalParts) continue; + + part = info->normalParts[batchOffset]; + if (part.offset < 0) continue; + hasNormParts[batch] = true; + +#ifndef CC_BUILD_GL11 + Gfx_BindVb_Textured(info->vb); +#endif + + offset = part.offset + part.spriteCount; + drawMin = info->drawXMin && part.counts[FACE_XMIN]; + drawMax = info->drawXMax && part.counts[FACE_XMAX]; + DrawNormalFaces(FACE_XMIN, FACE_XMAX); + + offset += part.counts[FACE_XMIN] + part.counts[FACE_XMAX]; + drawMin = info->drawZMin && part.counts[FACE_ZMIN]; + drawMax = info->drawZMax && part.counts[FACE_ZMAX]; + DrawNormalFaces(FACE_ZMIN, FACE_ZMAX); + + offset += part.counts[FACE_ZMIN] + part.counts[FACE_ZMAX]; + drawMin = info->drawYMin && part.counts[FACE_YMIN]; + drawMax = info->drawYMax && part.counts[FACE_YMAX]; + DrawNormalFaces(FACE_YMIN, FACE_YMAX); + + if (!part.spriteCount) continue; + offset = part.offset; + count = part.spriteCount >> 2; /* 4 per sprite */ + + Gfx_SetFaceCulling(true); + /* TODO: fix to not render them all */ +#ifdef CC_BUILD_GL11 + Gfx_BindVb(part.vbs[FACE_COUNT]); + Gfx_DrawIndexedTris_T2fC4b(0, 0); + Game_Vertices += count * 4; + Gfx_SetFaceCulling(false); + continue; +#endif + if (info->drawXMax || info->drawZMin) { + Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count; + } offset += count; + + if (info->drawXMin || info->drawZMax) { + Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count; + } offset += count; + + if (info->drawXMin || info->drawZMin) { + Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count; + } offset += count; + + if (info->drawXMax || info->drawZMax) { + Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count; + } + Gfx_SetFaceCulling(false); + } +} + +void MapRenderer_RenderNormal(float delta) { + int batch; + if (!mapChunks) return; + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Gfx_SetAlphaTest(true); + + Gfx_EnableMipmaps(); + for (batch = 0; batch < MapRenderer_1DUsedCount; batch++) { + if (normPartsCount[batch] <= 0) continue; + if (hasNormParts[batch] || checkNormParts[batch]) { + Gfx_BindTexture(Atlas1D.TexIds[batch]); + RenderNormalBatch(batch); + checkNormParts[batch] = false; + } + } + Gfx_DisableMipmaps(); + + CheckWeather(delta); + Gfx_SetAlphaTest(false); +#if DEBUG_OCCLUSION + DebugPickedPos(); +#endif +} + +#define DrawTranslucentFaces(minFace, maxFace) \ +if (drawMin && drawMax) { \ + DrawFaces(minFace, maxFace, offset); \ + Game_Vertices += (part.counts[minFace] + part.counts[maxFace]); \ +} else if (drawMin) { \ + DrawFace(minFace, offset); \ + Game_Vertices += part.counts[minFace]; \ +} else if (drawMax) { \ + DrawFace(maxFace, offset + part.counts[minFace]); \ + Game_Vertices += part.counts[maxFace]; \ +} + +static void RenderTranslucentBatch(int batch) { + int batchOffset = chunksCount * batch; + struct ChunkInfo* info; + struct ChunkPartInfo part; + cc_bool drawMin, drawMax; + int i, offset; + + for (i = 0; i < renderChunksCount; i++) { + info = renderChunks[i]; + if (!info->translucentParts) continue; + + part = info->translucentParts[batchOffset]; + if (part.offset < 0) continue; + hasTranParts[batch] = true; + +#ifndef CC_BUILD_GL11 + Gfx_BindVb_Textured(info->vb); +#endif + + offset = part.offset; + drawMin = (inTranslucent || info->drawXMin) && part.counts[FACE_XMIN]; + drawMax = (inTranslucent || info->drawXMax) && part.counts[FACE_XMAX]; + DrawTranslucentFaces(FACE_XMIN, FACE_XMAX); + + offset += part.counts[FACE_XMIN] + part.counts[FACE_XMAX]; + drawMin = (inTranslucent || info->drawZMin) && part.counts[FACE_ZMIN]; + drawMax = (inTranslucent || info->drawZMax) && part.counts[FACE_ZMAX]; + DrawTranslucentFaces(FACE_ZMIN, FACE_ZMAX); + + offset += part.counts[FACE_ZMIN] + part.counts[FACE_ZMAX]; + drawMin = (inTranslucent || info->drawYMin) && part.counts[FACE_YMIN]; + drawMax = (inTranslucent || info->drawYMax) && part.counts[FACE_YMAX]; + DrawTranslucentFaces(FACE_YMIN, FACE_YMAX); + } +} + +void MapRenderer_RenderTranslucent(float delta) { + int vertices, batch; + if (!mapChunks) return; + + /* First fill depth buffer */ + vertices = Game_Vertices; + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Gfx_SetAlphaBlending(false); + Gfx_DepthOnlyRendering(true); + + for (batch = 0; batch < MapRenderer_1DUsedCount; batch++) { + if (tranPartsCount[batch] <= 0) continue; + if (hasTranParts[batch] || checkTranParts[batch]) { + RenderTranslucentBatch(batch); + checkTranParts[batch] = false; + } + } + Game_Vertices = vertices; + + /* Then actually draw the transluscent blocks */ + Gfx_SetAlphaBlending(true); + Gfx_DepthOnlyRendering(false); + Gfx_SetDepthWrite(false); /* already calculated depth values in depth pass */ + + Gfx_EnableMipmaps(); + for (batch = 0; batch < MapRenderer_1DUsedCount; batch++) { + if (tranPartsCount[batch] <= 0) continue; + if (!hasTranParts[batch]) continue; + Gfx_BindTexture(Atlas1D.TexIds[batch]); + RenderTranslucentBatch(batch); + } + Gfx_DisableMipmaps(); + + Gfx_SetDepthWrite(true); + /* If we weren't under water, render weather after to blend properly */ + if (!inTranslucent && Env.Weather != WEATHER_SUNNY) { + Gfx_SetAlphaTest(true); + EnvRenderer_RenderWeather(delta); + Gfx_SetAlphaTest(false); + } + Gfx_SetAlphaBlending(false); +} + + +/*########################################################################################################################* +*---------------------------------------------------Chunk functionality---------------------------------------------------* +*#########################################################################################################################*/ +/* Deletes vertex buffer associated with the given chunk and updates internal state */ +static void DeleteChunk(struct ChunkInfo* info) { + struct ChunkPartInfo* ptr; + int i; +#ifdef CC_BUILD_GL11 + int j; +#else + Gfx_DeleteVb(&info->vb); +#endif + + info->empty = false; + info->allAir = false; + info->noData = true; +#ifdef OCCLUSION + info.OcclusionFlags = 0; + info.OccludedFlags = 0; +#endif + + if (info->normalParts) { + ptr = info->normalParts; + for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) { + if (ptr->offset < 0) continue; + normPartsCount[i]--; +#ifdef CC_BUILD_GL11 + for (j = 0; j < CHUNKPART_MAX_VBS; j++) Gfx_DeleteVb(&ptr->vbs[j]); +#endif + } + info->normalParts = NULL; + } + + if (info->translucentParts) { + ptr = info->translucentParts; + for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) { + if (ptr->offset < 0) continue; + tranPartsCount[i]--; +#ifdef CC_BUILD_GL11 + for (j = 0; j < CHUNKPART_MAX_VBS; j++) Gfx_DeleteVb(&ptr->vbs[j]); +#endif + } + info->translucentParts = NULL; + } +} + +/* Builds the mesh (hence vertex buffer) for the given chunk, and updates internal state */ +static void BuildChunk(struct ChunkInfo* info, int* chunkUpdates) { + struct ChunkPartInfo* ptr; + int i; + + Game.ChunkUpdates++; + (*chunkUpdates)++; + Builder_MakeChunk(info); + + info->dirty = false; + info->noData = !info->normalParts && !info->translucentParts; + info->empty = info->noData; + if (info->empty) return; + + if (info->normalParts) { + ptr = info->normalParts; + for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) { + if (ptr->offset >= 0) normPartsCount[i]++; + } + } + + if (info->translucentParts) { + ptr = info->translucentParts; + for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) { + if (ptr->offset >= 0) tranPartsCount[i]++; + } + } +} + + +/*########################################################################################################################* +*----------------------------------------------------Chunks mangagement---------------------------------------------------* +*#########################################################################################################################*/ +static void FreeParts(void) { + Mem_Free(MapRenderer_PartsNormal); + MapRenderer_PartsNormal = NULL; + MapRenderer_PartsTranslucent = NULL; +} + +static void FreeChunks(void) { + Mem_Free(mapChunks); + Mem_Free(sortedChunks); + Mem_Free(renderChunks); + Mem_Free(distances); + + mapChunks = NULL; + sortedChunks = NULL; + renderChunks = NULL; + distances = NULL; +} + +static void AllocateParts(void) { + struct ChunkPartInfo* ptr; + cc_uint32 count = chunksCount * MapRenderer_1DUsedCount; + + ptr = (struct ChunkPartInfo*)Mem_AllocCleared(count * 2, sizeof(struct ChunkPartInfo), "chunk parts"); + MapRenderer_PartsNormal = ptr; + MapRenderer_PartsTranslucent = ptr + count; +} + +static void AllocateChunks(void) { + mapChunks = (struct ChunkInfo*) Mem_Alloc(chunksCount, sizeof(struct ChunkInfo), "chunk info"); + sortedChunks = (struct ChunkInfo**)Mem_Alloc(chunksCount, sizeof(struct ChunkInfo*), "sorted chunk info"); + renderChunks = (struct ChunkInfo**)Mem_Alloc(chunksCount, sizeof(struct ChunkInfo*), "render chunk info"); + distances = (cc_uint32*)Mem_Alloc(chunksCount, 4, "chunk distances"); +} + +static void ResetPartFlags(void) { + int i; + for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) { + checkNormParts[i] = true; + hasNormParts[i] = false; + checkTranParts[i] = true; + hasTranParts[i] = false; + } +} + +static void ResetPartCounts(void) { + int i; + for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) { + normPartsCount[i] = 0; + tranPartsCount[i] = 0; + } +} + +static void InitChunks(void) { + int x, y, z, index = 0; + for (z = 0; z < World.Length; z += CHUNK_SIZE) { + for (y = 0; y < World.Height; y += CHUNK_SIZE) { + for (x = 0; x < World.Width; x += CHUNK_SIZE) { + ChunkInfo_Reset(&mapChunks[index], x, y, z); + sortedChunks[index] = &mapChunks[index]; + renderChunks[index] = &mapChunks[index]; + distances[index] = 0; + index++; + } + } + } +} + +static void ResetChunks(void) { + int x, y, z, index = 0; + for (z = 0; z < World.Length; z += CHUNK_SIZE) { + for (y = 0; y < World.Height; y += CHUNK_SIZE) { + for (x = 0; x < World.Width; x += CHUNK_SIZE) { + ChunkInfo_Reset(&mapChunks[index], x, y, z); + index++; + } + } + } +} + +static void DeleteChunks(void) { + int i; + if (!mapChunks) return; + + for (i = 0; i < chunksCount; i++) { + DeleteChunk(&mapChunks[i]); + } + ResetPartCounts(); +} + +void MapRenderer_Refresh(void) { + int oldCount; + chunkPos = IVec3_MaxValue(); + + if (mapChunks && World.Blocks) { + DeleteChunks(); + ResetChunks(); + + oldCount = MapRenderer_1DUsedCount; + MapRenderer_1DUsedCount = MapRenderer_UsedAtlases(); + /* Need to reallocate parts array in this case */ + if (MapRenderer_1DUsedCount != oldCount) { + FreeParts(); + AllocateParts(); + } + } + ResetPartCounts(); +} + +/* Refreshes chunks on the border of the map whose y is less than 'maxHeight'. */ +static void RefreshBorderChunks(int maxHeight) { + int cx, cy, cz; + cc_bool onBorder; + + chunkPos = IVec3_MaxValue(); + if (!mapChunks || !World.Blocks) return; + + for (cz = 0; cz < World.ChunksZ; cz++) { + for (cy = 0; cy < World.ChunksY; cy++) { + for (cx = 0; cx < World.ChunksX; cx++) { + onBorder = cx == 0 || cz == 0 || cx == (World.ChunksX - 1) || cz == (World.ChunksZ - 1); + + if (onBorder && (cy * CHUNK_SIZE) < maxHeight) { + MapRenderer_RefreshChunk(cx, cy, cz); + } + } + } + } +} + + +/*########################################################################################################################* +*--------------------------------------------------Chunks updating/sorting------------------------------------------------* +*#########################################################################################################################*/ +#define CHUNK_TARGET_TIME ((1.0f/30) + 0.01f) +static int chunksTarget = 12; +static Vec3 lastCamPos; +static float lastYaw, lastPitch; +/* Max distance from camera that chunks are rendered within */ +/* This may differ from the view distance configured by the user */ +static int renderDistSquared; +/* Max distance from camera that chunks are built within */ +/* Chunks past this distance are automatically unloaded */ +static int buildDistSquared; + +static int AdjustDist(int dist) { + if (dist < CHUNK_SIZE) dist = CHUNK_SIZE; + dist = Utils_AdjViewDist(dist); + return (dist + 24) * (dist + 24); +} + +static void CalcViewDists(void) { + buildDistSquared = AdjustDist(Game_UserViewDistance); + renderDistSquared = AdjustDist(Game_ViewDistance); +} + +static int UpdateChunksAndVisibility(int* chunkUpdates) { + int renderDistSqr = renderDistSquared; + int buildDistSqr = buildDistSquared; + + struct ChunkInfo* info; + int i, j = 0, distSqr; + cc_bool noData; + + for (i = 0; i < chunksCount; i++) { + info = sortedChunks[i]; + if (info->empty) continue; + + distSqr = distances[i]; + noData = info->noData; + + /* Auto unload chunks far away chunks */ + if (!noData && distSqr >= buildDistSqr + 32 * 16) { + DeleteChunk(info); continue; + } + noData |= info->dirty; + + if (noData && distSqr <= buildDistSqr && *chunkUpdates < chunksTarget) { + DeleteChunk(info); + BuildChunk(info, chunkUpdates); + } + + info->visible = distSqr <= renderDistSqr && + FrustumCulling_SphereInFrustum(info->centreX, info->centreY, info->centreZ, 14); /* 14 ~ sqrt(3 * 8^2) */ + if (info->visible && !info->empty) { renderChunks[j] = info; j++; } + } + return j; +} + +static int UpdateChunksStill(int* chunkUpdates) { + int renderDistSqr = renderDistSquared; + int buildDistSqr = buildDistSquared; + + struct ChunkInfo* info; + int i, j = 0, distSqr; + cc_bool noData; + + for (i = 0; i < chunksCount; i++) { + info = sortedChunks[i]; + if (info->empty) continue; + + distSqr = distances[i]; + noData = info->noData; + + /* Auto unload chunks far away chunks */ + if (!noData && distSqr >= buildDistSqr + 32 * 16) { + DeleteChunk(info); continue; + } + noData |= info->dirty; + + if (noData && distSqr <= buildDistSqr && *chunkUpdates < chunksTarget) { + DeleteChunk(info); + BuildChunk(info, chunkUpdates); + + /* only need to update the visibility of chunks in range. */ + info->visible = distSqr <= renderDistSqr && + FrustumCulling_SphereInFrustum(info->centreX, info->centreY, info->centreZ, 14); /* 14 ~ sqrt(3 * 8^2) */ + if (info->visible && !info->empty) { renderChunks[j] = info; j++; } + } else if (info->visible) { + renderChunks[j] = info; j++; + } + } + return j; +} + +static void UpdateChunks(float delta) { + struct LocalPlayer* p; + cc_bool samePos; + int chunkUpdates = 0; + + /* Build more chunks if 30 FPS or over, otherwise slowdown */ + chunksTarget += delta < CHUNK_TARGET_TIME ? 1 : -1; + Math_Clamp(chunksTarget, 4, maxChunkUpdates); + + p = Entities.CurPlayer; + samePos = Vec3_Equals(&Camera.CurrentPos, &lastCamPos) + && p->Base.Pitch == lastPitch && p->Base.Yaw == lastYaw; + + renderChunksCount = samePos ? + UpdateChunksStill(&chunkUpdates) : + UpdateChunksAndVisibility(&chunkUpdates); + + lastCamPos = Camera.CurrentPos; + lastPitch = p->Base.Pitch; + lastYaw = p->Base.Yaw; + + if (!samePos || chunkUpdates) ResetPartFlags(); +} + +static void SortMapChunks(int left, int right) { + struct ChunkInfo** values = sortedChunks; struct ChunkInfo* value; + cc_uint32* keys = distances; cc_uint32 key; + + while (left < right) { + int i = left, j = right; + cc_uint32 pivot = keys[(i + j) >> 1]; + + /* partition the list */ + while (i <= j) { + while (pivot > keys[i]) i++; + while (pivot < keys[j]) j--; + QuickSort_Swap_KV_Maybe(); + } + /* recurse into the smaller subset */ + QuickSort_Recurse(SortMapChunks) + } +} + +static void UpdateSortOrder(void) { + struct ChunkInfo* info; + IVec3 pos; + int i, dx, dy, dz; + + /* pos is centre coordinate of chunk camera is in */ + IVec3_Floor(&pos, &Camera.CurrentPos); + pos.x = (pos.x & ~CHUNK_MASK) + HALF_CHUNK_SIZE; + pos.y = (pos.y & ~CHUNK_MASK) + HALF_CHUNK_SIZE; + pos.z = (pos.z & ~CHUNK_MASK) + HALF_CHUNK_SIZE; + + /* If in same chunk, don't need to recalculate sort order */ + if (pos.x == chunkPos.x && pos.y == chunkPos.y && pos.z == chunkPos.z) return; + chunkPos = pos; + if (!chunksCount) return; + + for (i = 0; i < chunksCount; i++) { + info = sortedChunks[i]; + /* Calculate distance to chunk centre */ + dx = info->centreX - pos.x; dy = info->centreY - pos.y; dz = info->centreZ - pos.z; + distances[i] = dx * dx + dy * dy + dz * dz; + + /* Consider these 3 chunks: */ + /* | X-1 | X | X+1 | */ + /* |################|########@########|################| */ + /* Assume the player is standing at @, then DrawXMin/XMax is calculated as this */ + /* X-1: DrawXMin = false, DrawXMax = true */ + /* X : DrawXMin = true, DrawXMax = true */ + /* X+1: DrawXMin = true, DrawXMax = false */ + + info->drawXMin = dx >= 0; info->drawXMax = dx <= 0; + info->drawZMin = dz >= 0; info->drawZMax = dz <= 0; + info->drawYMin = dy >= 0; info->drawYMax = dy <= 0; + } + + SortMapChunks(0, chunksCount - 1); + ResetPartFlags(); + /*SimpleOcclusionCulling();*/ +} + +void MapRenderer_Update(float delta) { + if (!mapChunks) return; + UpdateSortOrder(); + UpdateChunks(delta); +} + + +/*########################################################################################################################* +*---------------------------------------------------------General---------------------------------------------------------* +*#########################################################################################################################*/ +void MapRenderer_RefreshChunk(int cx, int cy, int cz) { + struct ChunkInfo* info; + if (cx < 0 || cy < 0 || cz < 0 || cx >= World.ChunksX || cy >= World.ChunksY || cz >= World.ChunksZ) return; + + info = &mapChunks[World_ChunkPack(cx, cy, cz)]; + if (info->allAir) return; /* do not recreate chunks completely air */ + info->empty = false; + info->dirty = true; +} + +void MapRenderer_OnBlockChanged(int x, int y, int z, BlockID block) { + int cx = x >> CHUNK_SHIFT, cy = y >> CHUNK_SHIFT, cz = z >> CHUNK_SHIFT; + struct ChunkInfo* chunk; + + chunk = &mapChunks[World_ChunkPack(cx, cy, cz)]; + chunk->allAir &= Blocks.Draw[block] == DRAW_GAS; + /* TODO: Don't lookup twice, refresh directly using chunk pointer */ + MapRenderer_RefreshChunk(cx, cy, cz); +} + +static void OnEnvVariableChanged(void* obj, int envVar) { + if (envVar == ENV_VAR_SUN_COLOR || envVar == ENV_VAR_SHADOW_COLOR) { + MapRenderer_Refresh(); + } else if (envVar == ENV_VAR_EDGE_HEIGHT || envVar == ENV_VAR_SIDES_OFFSET) { + int oldClip = Builder_EdgeLevel; + Builder_SidesLevel = max(0, Env_SidesHeight); + Builder_EdgeLevel = max(0, Env.EdgeHeight); + + /* Only need to refresh chunks on map borders up to highest edge level.*/ + RefreshBorderChunks(max(oldClip, Builder_EdgeLevel)); + } +} + +static void OnTerrainAtlasChanged(void* obj) { + static int tilesPerAtlas; + /* e.g. If old atlas was 256x256 and new is 256x256, don't need to refresh */ + if (MapRenderer_1DUsedCount && tilesPerAtlas != Atlas1D.TilesPerAtlas) { + MapRenderer_Refresh(); + } + + MapRenderer_1DUsedCount = MapRenderer_UsedAtlases(); + tilesPerAtlas = Atlas1D.TilesPerAtlas; + ResetPartFlags(); +} + +static void OnBlockDefinitionChanged(void* obj) { + MapRenderer_Refresh(); + MapRenderer_1DUsedCount = MapRenderer_UsedAtlases(); + ResetPartFlags(); +} + +static void OnVisibilityChanged(void* obj) { + lastCamPos = Vec3_BigPos(); + CalcViewDists(); +} +static void DeleteChunks_(void* obj) { DeleteChunks(); } +static void Refresh_(void* obj) { MapRenderer_Refresh(); } + +static void OnNewMap(void) { + Game.ChunkUpdates = 0; + DeleteChunks(); + ResetPartCounts(); + + chunkPos = IVec3_MaxValue(); + FreeChunks(); + FreeParts(); +} + +static void OnNewMapLoaded(void) { + chunksCount = World.ChunksCount; + /* TODO: Only perform reallocation when map volume has changed */ + /*if (chunksCount != World.ChunksCount) { */ + /* chunksCount = World.ChunksCount; */ + FreeChunks(); + FreeParts(); + AllocateChunks(); + AllocateParts(); + /*}*/ + + InitChunks(); + lastCamPos = Vec3_BigPos(); +} + +static void OnInit(void) { + Event_Register_(&TextureEvents.AtlasChanged, NULL, OnTerrainAtlasChanged); + Event_Register_(&WorldEvents.EnvVarChanged, NULL, OnEnvVariableChanged); + Event_Register_(&BlockEvents.BlockDefChanged, NULL, OnBlockDefinitionChanged); + + Event_Register_(&GfxEvents.ViewDistanceChanged, NULL, OnVisibilityChanged); + Event_Register_(&GfxEvents.ProjectionChanged, NULL, OnVisibilityChanged); + Event_Register_(&GfxEvents.ContextLost, NULL, DeleteChunks_); + Event_Register_(&GfxEvents.ContextRecreated, NULL, Refresh_); + + /* This = 87 fixes map being invisible when no textures */ + MapRenderer_1DUsedCount = 87; /* Atlas1D_UsedAtlasesCount(); */ + chunkPos = IVec3_MaxValue(); + maxChunkUpdates = Options_GetInt(OPT_MAX_CHUNK_UPDATES, 4, 1024, 30); + CalcViewDists(); +} + +struct IGameComponent MapRenderer_Component = { + OnInit, /* Init */ + OnNewMap, /* Free */ + OnNewMap, /* Reset */ + OnNewMap, /* OnNewMap */ + OnNewMapLoaded /* OnNewMapLoaded */ +}; diff --git a/src/MapRenderer.h b/src/MapRenderer.h new file mode 100644 index 0000000..d6361c7 --- /dev/null +++ b/src/MapRenderer.h @@ -0,0 +1,78 @@ +#ifndef CC_MAPRENDERER_H +#define CC_MAPRENDERER_H +#include "Core.h" +#include "Constants.h" +/* Renders the blocks of the world by subdividing it into chunks. + Also manages the process of building/deleting chunk meshes. + Also sorts chunks so nearest chunks are rendered first, and calculates chunk visibility. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct IGameComponent; +extern struct IGameComponent MapRenderer_Component; + +/* Max used 1D atlases. (i.e. Atlas1D_Index(maxTextureLoc) + 1) */ +extern int MapRenderer_1DUsedCount; + +/* Buffer for all chunk parts. There are (MapRenderer_ChunksCount * Atlas1D_Count) parts in the buffer, +with parts for 'normal' buffer being in lower half. */ +extern struct ChunkPartInfo* MapRenderer_PartsNormal; /* TODO: THAT DESC SUCKS */ +extern struct ChunkPartInfo* MapRenderer_PartsTranslucent; + +/* Describes a portion of the data needed for rendering a chunk. */ +struct ChunkPartInfo { +#ifdef CC_BUILD_GL11 + /* 1 VB per face, another VB for sprites */ + #define CHUNKPART_MAX_VBS (FACE_COUNT + 1) + GfxResourceID vbs[CHUNKPART_MAX_VBS]; +#endif + int offset; /* -1 if no vertices at all */ + int spriteCount; /* Sprite vertices count */ + cc_uint16 counts[FACE_COUNT]; /* Counts per face */ +}; + +/* Describes data necessary for rendering a chunk. */ +struct ChunkInfo { + cc_uint16 centreX, centreY, centreZ; /* Centre coordinates of the chunk */ + + cc_uint8 visible : 1; /* Whether chunk is visible to the player */ + cc_uint8 empty : 1; /* Whether the chunk is empty of data and is known to have no data */ + cc_uint8 dirty : 1; /* Whether chunk is pending being rebuilt */ + cc_uint8 allAir : 1; /* Whether chunk is completely air */ + cc_uint8 noData : 1; /* Whether the chunk is currently empty of data, but may have data if built */ + cc_uint8 : 0; /* pad to next byte*/ + + cc_uint8 drawXMin : 1; + cc_uint8 drawXMax : 1; + cc_uint8 drawZMin : 1; + cc_uint8 drawZMax : 1; + cc_uint8 drawYMin : 1; + cc_uint8 drawYMax : 1; + cc_uint8 : 0; /* pad to next byte */ +#ifdef OCCLUSION + public cc_bool Visited = false, Occluded = false; + public byte OcclusionFlags, OccludedFlags, DistanceFlags; +#endif +#ifndef CC_BUILD_GL11 + GfxResourceID vb; +#endif + struct ChunkPartInfo* normalParts; + struct ChunkPartInfo* translucentParts; +}; + +/* Renders the meshes of non-translucent blocks in visible chunks. */ +void MapRenderer_RenderNormal(float delta); +/* Renders the meshes of translucent blocks in visible chunks. */ +void MapRenderer_RenderTranslucent(float delta); +/* Potentially updates sort order of rendered chunks. */ +/* Potentially builds meshes for several nearby chunks. */ +/* NOTE: This should be called once per frame. */ +void MapRenderer_Update(float delta); + +/* Marks the given chunk as needing to be rebuilt/redrawn. */ +/* NOTE: Coordinates outside the map are simply ignored. */ +void MapRenderer_RefreshChunk(int cx, int cy, int cz); +/* Called when a block is changed, to update internal state. */ +void MapRenderer_OnBlockChanged(int x, int y, int z, BlockID block); +/* Deletes all chunks and resets internal state. */ +void MapRenderer_Refresh(void); +#endif diff --git a/src/Menus.c b/src/Menus.c new file mode 100644 index 0000000..7e18b78 --- /dev/null +++ b/src/Menus.c @@ -0,0 +1,4042 @@ +#include "Menus.h" +#include "Widgets.h" +#include "Game.h" +#include "Event.h" +#include "Platform.h" +#include "Inventory.h" +#include "Drawer2D.h" +#include "Graphics.h" +#include "Funcs.h" +#include "Model.h" +#include "Generator.h" +#include "Server.h" +#include "Chat.h" +#include "ExtMath.h" +#include "Window.h" +#include "Camera.h" +#include "Http.h" +#include "Block.h" +#include "World.h" +#include "Formats.h" +#include "BlockPhysics.h" +#include "MapRenderer.h" +#include "TexturePack.h" +#include "Audio.h" +#include "Screens.h" +#include "Gui.h" +#include "Deflate.h" +#include "Stream.h" +#include "Builder.h" +#include "Lighting.h" +#include "Logger.h" +#include "Options.h" +#include "Input.h" +#include "Utils.h" +#include "Errors.h" +#include "SystemFonts.h" +#include "Lighting.h" + +/* Describes a menu option button */ +struct MenuOptionDesc { + short dir, y; + const char* name; + Widget_LeftClick OnClick; + Button_Get GetValue; Button_Set SetValue; +}; + + +/*########################################################################################################################* +*--------------------------------------------------------Menu base--------------------------------------------------------* +*#########################################################################################################################*/ +void Menu_AddButtons(void* screen, struct ButtonWidget* btns, int width, const struct SimpleButtonDesc* descs, int count) { + int i; + for (i = 0; i < count; i++) { + ButtonWidget_Add(screen, &btns[i], width, descs[i].onClick); + } +} + +void Menu_LayoutButtons(struct ButtonWidget* btns, const struct SimpleButtonDesc* descs, int count) { + int i; + for (i = 0; i < count; i++) { + Widget_SetLocation(&btns[i], ANCHOR_CENTRE, ANCHOR_CENTRE, descs[i].x, descs[i].y); + } +} + +void Menu_SetButtons(struct ButtonWidget* btns, struct FontDesc* font, const struct SimpleButtonDesc* descs, int count) { + int i; + for (i = 0; i < count; i++) { + ButtonWidget_SetConst(&btns[i], descs[i].title, font); + } +} + +void Menu_LayoutBack(struct ButtonWidget* btn) { + Widget_SetLocation(btn, ANCHOR_CENTRE, ANCHOR_MAX, 0, 25); +} +static void Menu_CloseKeyboard(void* s) { OnscreenKeyboard_Close(); } + +static void Menu_RenderBounds(void) { + /* These were sourced by taking a screenshot of vanilla + Then using paint to extract the color components + Then using wolfram alpha to solve the glblendfunc equation */ + PackedCol topCol = PackedCol_Make(24, 24, 24, 105); + PackedCol bottomCol = PackedCol_Make(51, 51, 98, 162); + Gfx_Draw2DGradient(0, 0, Window_UI.Width, Window_UI.Height, topCol, bottomCol); +} + + +static void Menu_UnselectAll(struct Screen* s) { + int i; + /* TODO: pointer ID mask */ + + for (i = 0; i < s->numWidgets; i++) + { + struct Widget* w = s->widgets[i]; + if (w) w->active = false; + } +} + +int Menu_PointerDown(void* screen, int id, int x, int y) { + Screen_DoPointerDown(screen, id, x, y); + return TOUCH_TYPE_GUI; +} + +static int Menu_DoPointerMove(void* screen, int id, int x, int y) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets; + int i, count; + + Menu_UnselectAll(s); + widgets = s->widgets; + count = s->numWidgets; + + for (i = count - 1; i >= 0; i--) + { + struct Widget* w = widgets[i]; + if (!w || !Widget_Contains(w, x, y)) continue; + + w->active = true; + return i; + } + return -1; +} + +int Menu_PointerMove(void* screen, int id, int x, int y) { + Menu_DoPointerMove(screen, id, x, y); return true; +} + +static cc_bool Menu_IsSelectable(struct Widget* w) { + if (!w || (w->flags & WIDGET_FLAG_DISABLED)) return false; + if (!(w->flags & WIDGET_FLAG_SELECTABLE)) return false; + + return true; +} + +static void Menu_CycleSelected(struct Screen* s, int dir) { + struct Widget* w; + int index = s->selectedI + dir; + int i, j; + + for (j = 0; j < s->numWidgets; j++) + { + i = (index + j * dir) % s->numWidgets; + if (i < 0) i += s->numWidgets; + + w = s->widgets[i]; + if (!Menu_IsSelectable(w)) continue; + + Menu_UnselectAll(s); + s->selectedI = i; + w->active = true; + return; + } +} + +static void Menu_ClickSelected(struct Screen* s) { + struct Widget* w; + if (s->selectedI < 0) return; + w = s->widgets[s->selectedI]; + + if (!Menu_IsSelectable(w)) return; + if (w->MenuClick) w->MenuClick(s, w); +} + +int Menu_InputDown(void* screen, int key) { + struct Screen* s = (struct Screen*)screen; + + if (Input_IsUpButton(key)) { + Menu_CycleSelected(s, -1); + } else if (Input_IsDownButton(key)) { + Menu_CycleSelected(s, +1); + } else if (Input_IsEnterButton(key)) { + Menu_ClickSelected(s); + } + return Screen_InputDown(screen, key); +} + + +/*########################################################################################################################* +*------------------------------------------------------Menu utilities-----------------------------------------------------* +*#########################################################################################################################*/ +static void Menu_Remove(void* screen, int i) { + struct Screen* s = (struct Screen*)screen; + struct Widget** widgets = s->widgets; + + if (widgets[i]) { Elem_Free(widgets[i]); } + widgets[i] = NULL; +} + +static void Menu_BeginGen(int width, int height, int length) { + World_NewMap(); + World_SetDimensions(width, height, length); + + Gen_Start(); + GeneratingScreen_Show(); +} + +static int Menu_Int(const cc_string* str) { int v; Convert_ParseInt(str, &v); return v; } +static float Menu_Float(const cc_string* str) { float v; Convert_ParseFloat(str, &v); return v; } +static PackedCol Menu_HexCol(const cc_string* str) { + cc_uint8 rgb[3]; + PackedCol_TryParseHex(str, rgb); + return PackedCol_Make(rgb[0], rgb[1], rgb[2], 255); +} + +static void Menu_SwitchOptions(void* a, void* b) { OptionsGroupScreen_Show(); } +static void Menu_SwitchPause(void* a, void* b) { Gui_ShowPauseMenu(); } +static void Menu_SwitchClassicOptions(void* a, void* b) { ClassicOptionsScreen_Show(); } + +static void Menu_SwitchBindsClassic(void* a, void* b) { ClassicBindingsScreen_Show(); } +static void Menu_SwitchBindsClassicHacks(void* a, void* b) { ClassicHacksBindingsScreen_Show(); } +static void Menu_SwitchBindsNormal(void* a, void* b) { NormalBindingsScreen_Show(); } +static void Menu_SwitchBindsHacks(void* a, void* b) { HacksBindingsScreen_Show(); } +static void Menu_SwitchBindsOther(void* a, void* b) { OtherBindingsScreen_Show(); } +static void Menu_SwitchBindsMouse(void* a, void* b) { MouseBindingsScreen_Show(); } +static void Menu_SwitchBindsHotbar(void* a, void* b) { HotbarBindingsScreen_Show(); } +static void SwitchBindsMain(void* s, void* w); + +static void Menu_SwitchMisc(void* a, void* b) { MiscOptionsScreen_Show(); } +static void Menu_SwitchChat(void* a, void* b) { ChatOptionsScreen_Show(); } +static void Menu_SwitchGui(void* a, void* b) { GuiOptionsScreen_Show(); } +static void Menu_SwitchGfx(void* a, void* b) { GraphicsOptionsScreen_Show(); } +static void Menu_SwitchHacks(void* a, void* b) { HacksSettingsScreen_Show(); } +static void Menu_SwitchEnv(void* a, void* b) { EnvSettingsScreen_Show(); } +static void Menu_SwitchNostalgia(void* a, void* b) { NostalgiaMenuScreen_Show(); } + +static void Menu_SwitchGenLevel(void* a, void* b) { GenLevelScreen_Show(); } +static void Menu_SwitchClassicGenLevel(void* a, void* b) { ClassicGenScreen_Show(); } +static void Menu_SwitchLoadLevel(void* a, void* b) { LoadLevelScreen_Show(); } +static void Menu_SwitchSaveLevel(void* a, void* b) { SaveLevelScreen_Show(); } +static void Menu_SwitchTexPacks(void* a, void* b) { TexturePackScreen_Show(); } +static void Menu_SwitchHotkeys(void* a, void* b) { HotkeyListScreen_Show(); } +static void Menu_SwitchFont(void* a, void* b) { FontListScreen_Show(); } + + +/*########################################################################################################################* +*--------------------------------------------------------ListScreen-------------------------------------------------------* +*#########################################################################################################################*/ +struct ListScreen; +#define LIST_SCREEN_ITEMS 5 + +static struct ListScreen { + Screen_Body + struct ButtonWidget btns[LIST_SCREEN_ITEMS]; + struct ButtonWidget left, right, done, action; + struct FontDesc font; + int currentIndex; + Widget_LeftClick EntryClick, DoneClick, ActionClick; + const char* actionText; + void (*LoadEntries)(struct ListScreen* s); + void (*UpdateEntry)(struct ListScreen* s, struct ButtonWidget* btn, const cc_string* text); + const char* titleText; + struct TextWidget title; + struct StringsBuffer entries; +} ListScreen; + +static struct Widget* list_widgets[LIST_SCREEN_ITEMS + 4 + 1]; +#define LISTSCREEN_EMPTY "-" + +static void ListScreen_Layout(void* screen) { + struct ListScreen* s = (struct ListScreen*)screen; + int i; + + for (i = 0; i < LIST_SCREEN_ITEMS; i++) { + Widget_SetLocation(&s->btns[i], + ANCHOR_CENTRE, ANCHOR_CENTRE, 0, (i - 2) * 50); + } + + if (Input_TouchMode) { + Widget_SetLocation(&s->done, ANCHOR_CENTRE_MIN, ANCHOR_MAX, -150, 25); + Widget_SetLocation(&s->action, ANCHOR_CENTRE_MAX, ANCHOR_MAX, -150, 25); + } else { + Widget_SetLocation(&s->done, ANCHOR_CENTRE, ANCHOR_MAX, 0, 25); + Widget_SetLocation(&s->action, ANCHOR_CENTRE, ANCHOR_MAX, 0, 70); + } + + Widget_SetLocation(&s->left, ANCHOR_CENTRE, ANCHOR_CENTRE, -220, 0); + Widget_SetLocation(&s->right, ANCHOR_CENTRE, ANCHOR_CENTRE, 220, 0); + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -155); +} + +static STRING_REF cc_string ListScreen_UNSAFE_Get(struct ListScreen* s, int index) { + static const cc_string str = String_FromConst(LISTSCREEN_EMPTY); + + if (index >= 0 && index < s->entries.count) { + return StringsBuffer_UNSAFE_Get(&s->entries, index); + } + return str; +} + +static void ListScreen_UpdateTitle(struct ListScreen* s) { + cc_string str; char strBuffer[STRING_SIZE]; + int num, pages; + String_InitArray(str, strBuffer); + String_AppendConst(&str, s->titleText); + + if (!Game_ClassicMode) { + num = (s->currentIndex / LIST_SCREEN_ITEMS) + 1; + pages = Math_CeilDiv(s->entries.count, LIST_SCREEN_ITEMS); + + if (pages == 0) pages = 1; + String_Format2(&str, " &7(page %i/%i)", &num, &pages); + } + TextWidget_Set(&s->title, &str, &s->font); +} + +static void ListScreen_UpdatePage(struct ListScreen* s) { + int end = s->entries.count - LIST_SCREEN_ITEMS; + Widget_SetDisabled(&s->left, s->currentIndex <= 0); + Widget_SetDisabled(&s->right, s->currentIndex >= end); + ListScreen_UpdateTitle(s); +} + +static void ListScreen_UpdateEntry(struct ListScreen* s, struct ButtonWidget* button, const cc_string* text) { + ButtonWidget_Set(button, text, &s->font); +} + +static void ListScreen_RedrawEntries(struct ListScreen* s) { + cc_string str; + int i; + for (i = 0; i < LIST_SCREEN_ITEMS; i++) { + str = ListScreen_UNSAFE_Get(s, s->currentIndex + i); + + Widget_SetDisabled(&s->btns[i], String_CaselessEqualsConst(&str, LISTSCREEN_EMPTY)); + s->UpdateEntry(s, &s->btns[i], &str); + } +} + +static void ListScreen_SetCurrentIndex(struct ListScreen* s, int index) { + if (index >= s->entries.count) { index = s->entries.count - 1; } + if (index < 0) index = 0; + + s->currentIndex = index; + ListScreen_RedrawEntries(s); + ListScreen_UpdatePage(s); +} + +static void ListScreen_PageClick(struct ListScreen* s, cc_bool forward) { + int delta = forward ? LIST_SCREEN_ITEMS : -LIST_SCREEN_ITEMS; + ListScreen_SetCurrentIndex(s, s->currentIndex + delta); +} + +static void ListScreen_MoveBackwards(void* screen, void* b) { + struct ListScreen* s = (struct ListScreen*)screen; + ListScreen_PageClick(s, false); +} + +static void ListScreen_MoveForwards(void* screen, void* b) { + struct ListScreen* s = (struct ListScreen*)screen; + ListScreen_PageClick(s, true); +} + +static cc_string ListScreen_UNSAFE_GetCur(struct ListScreen* s, void* widget) { + struct ButtonWidget* btn = (struct ButtonWidget*)widget; + int i = btn->meta.val; + return ListScreen_UNSAFE_Get(s, s->currentIndex + i); +} + +static void ListScreen_Select(struct ListScreen* s, const cc_string* str) { + cc_string entry; + int i; + + for (i = 0; i < s->entries.count; i++) { + entry = StringsBuffer_UNSAFE_Get(&s->entries, i); + if (!String_CaselessEquals(&entry, str)) continue; + + s->currentIndex = i; + return; + } +} + +static int ListScreen_KeyDown(void* screen, int key) { + struct ListScreen* s = (struct ListScreen*)screen; + + if (Input_IsLeftButton(key) || key == CCKEY_PAGEUP) { + ListScreen_PageClick(s, false); + } else if (Input_IsRightButton(key) || key == CCKEY_PAGEDOWN) { + ListScreen_PageClick(s, true); + } else if (key == CCWHEEL_UP) { + ListScreen_SetCurrentIndex(s, s->currentIndex - 1); + } else if (key == CCWHEEL_DOWN) { + ListScreen_SetCurrentIndex(s, s->currentIndex + 1); + } else { + Menu_InputDown(screen, key); + } + return true; +} + +static void ListScreen_Init(void* screen) { + struct ListScreen* s = (struct ListScreen*)screen; + int i, width; + s->widgets = list_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(list_widgets); + s->currentIndex = 0; + + for (i = 0; i < LIST_SCREEN_ITEMS; i++) + { + ButtonWidget_Add(s, &s->btns[i], 300, s->EntryClick); + s->btns[i].meta.val = i; + } + width = Input_TouchMode ? 140 : 400; + ButtonWidget_Add(s, &s->action, width, s->ActionClick); + + ButtonWidget_Add(s, &s->left, 40, ListScreen_MoveBackwards); + ButtonWidget_Add(s, &s->right, 40, ListScreen_MoveForwards); + TextWidget_Add(s, &s->title); + ButtonWidget_Add(s, &s->done, width, s->DoneClick); + + s->maxVertices = Screen_CalcDefaultMaxVertices(screen); + s->LoadEntries(s); +} + +static void ListScreen_Render(void* screen, float delta) { + Menu_RenderBounds(); + Screen_Render2Widgets(screen, delta); +} + +static void ListScreen_Free(void* screen) { + struct ListScreen* s = (struct ListScreen*)screen; + StringsBuffer_Clear(&s->entries); +} + +static void ListScreen_ContextLost(void* screen) { + struct ListScreen* s = (struct ListScreen*)screen; + Screen_ContextLost(screen); + Font_Free(&s->font); +} + +static void ListScreen_ContextRecreated(void* screen) { + struct ListScreen* s = (struct ListScreen*)screen; + Screen_UpdateVb(screen); + Gui_MakeTitleFont(&s->font); + ListScreen_RedrawEntries(s); + + ButtonWidget_SetConst(&s->left, "<", &s->font); + ButtonWidget_SetConst(&s->right, ">", &s->font); + ButtonWidget_SetConst(&s->done, "Done", &s->font); + ListScreen_UpdatePage(s); + + ButtonWidget_SetConst(&s->action, s->actionText, &s->font); +} + +static void ListScreen_Reload(struct ListScreen* s) { + ListScreen_Free(s); + s->LoadEntries(s); + ListScreen_SetCurrentIndex(s, s->currentIndex); +} + +static const struct ScreenVTABLE ListScreen_VTABLE = { + ListScreen_Init, Screen_NullUpdate, ListScreen_Free, + ListScreen_Render, Screen_BuildMesh, + ListScreen_KeyDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + ListScreen_Layout, ListScreen_ContextLost, ListScreen_ContextRecreated +}; +void ListScreen_Show(void) { + struct ListScreen* s = &ListScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &ListScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*--------------------------------------------------------MenuScreen-------------------------------------------------------* +*#########################################################################################################################*/ +void MenuScreen_Render2(void* screen, float delta) { + Menu_RenderBounds(); + Screen_Render2Widgets(screen, delta); +} + + +/*########################################################################################################################* +*-----------------------------------------------------PauseScreenBase-----------------------------------------------------* +*#########################################################################################################################*/ +#define PAUSE_MAX_BTNS 6 +static struct PauseScreen { + Screen_Body + int descsCount; + const struct SimpleButtonDesc* descs; + struct ButtonWidget btns[PAUSE_MAX_BTNS], quit, back; + struct TextWidget title, title2; +} PauseScreen; + +static void PauseScreenBase_Quit(void* a, void* b) { Window_RequestClose(); } +static void PauseScreenBase_Game(void* a, void* b) { Gui_Remove((struct Screen*)&PauseScreen); } + +static void PauseScreenBase_ContextRecreated(struct PauseScreen* s, struct FontDesc* titleFont) { + Screen_UpdateVb(s); + Gui_MakeTitleFont(titleFont); + //TODO - Make hack settings menu + Menu_SetButtons(s->btns, titleFont, s->descs, s->descsCount); + ButtonWidget_SetConst(&s->back, "Back to game", titleFont); + //TODO - Make credits be in bottom left + TextWidget_SetConst(&s->title, "&eClassihax by &dWlodekM", titleFont); + // TextWidget_SetConst(&s->title2, "&fGame menu", titleFont); +} + +static void PauseScreenBase_AddWidgets(struct PauseScreen* s, int width) { + TextWidget_Add(s, &s->title); + Menu_AddButtons(s, s->btns, width, s->descs, s->descsCount); + ButtonWidget_Add(s, &s->back, 400, PauseScreenBase_Game); +} + + +/*########################################################################################################################* +*-------------------------------------------------------PauseScreen-------------------------------------------------------* +*#########################################################################################################################*/ +static struct Widget* pause_widgets[1 + 6 + 2]; + +static void PauseScreen_CheckHacksAllowed(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + if (Gui.ClassicMenu) return; + + Widget_SetDisabled(&s->btns[1], + !Entities.CurPlayer->Hacks.CanAnyHacks); /* select texture pack */ + s->dirty = true; +} + +static void PauseScreen_ContextRecreated(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + struct FontDesc titleFont; + PauseScreenBase_ContextRecreated(s, &titleFont); + + ButtonWidget_SetConst(&s->quit, "Quit game", &titleFont); + PauseScreen_CheckHacksAllowed(s); + Font_Free(&titleFont); +} + +static void PauseScreen_Layout(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + Menu_LayoutButtons(s->btns, s->descs, s->descsCount); + Menu_LayoutBack(&s->back); + Widget_SetLocation(&s->quit, ANCHOR_MAX, ANCHOR_MAX, 5, 5); + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -100); +} + +static void PauseScreen_Init(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + static const struct SimpleButtonDesc descs[] = { + { -160, -50, "Options...", Menu_SwitchOptions }, + { -160, 0, "Change texture pack...", Menu_SwitchTexPacks }, + { -160, 50, "Hotkeys...", Menu_SwitchHotkeys }, + { 160, -50, "Generate new level...", Menu_SwitchGenLevel }, + { 160, 0, "Load level...", Menu_SwitchLoadLevel }, + { 160, 50, "Save level...", Menu_SwitchSaveLevel } + }; + s->widgets = pause_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(pause_widgets); + Event_Register_(&UserEvents.HackPermsChanged, s, PauseScreen_CheckHacksAllowed); + + s->descs = descs; + s->descsCount = Array_Elems(descs); + PauseScreenBase_AddWidgets(s, 300); + ButtonWidget_Add(s, &s->quit, 120, PauseScreenBase_Quit); + s->maxVertices = Screen_CalcDefaultMaxVertices(s); + + if (Server.IsSinglePlayer) return; + s->btns[3].flags = WIDGET_FLAG_DISABLED; + s->btns[4].flags = WIDGET_FLAG_DISABLED; +} + +static void PauseScreen_Free(void* screen) { + Event_Unregister_(&UserEvents.HackPermsChanged, screen, PauseScreen_CheckHacksAllowed); +} + +static const struct ScreenVTABLE PauseScreen_VTABLE = { + PauseScreen_Init, Screen_NullUpdate, PauseScreen_Free, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + PauseScreen_Layout, Screen_ContextLost, PauseScreen_ContextRecreated +}; +void PauseScreen_Show(void) { + struct PauseScreen* s = &PauseScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &PauseScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*----------------------------------------------------ClassicPauseScreen---------------------------------------------------* +*#########################################################################################################################*/ +static struct Widget* classicPause_widgets[1 + 5 + 1]; + +static void ClassicPauseScreen_ContextRecreated(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + struct FontDesc titleFont; + PauseScreenBase_ContextRecreated(s, &titleFont); + Font_Free(&titleFont); +} + +static void ClassicPauseScreen_Layout(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + Menu_LayoutButtons(s->btns, s->descs, s->descsCount); + Widget_SetLocation(&s->back, ANCHOR_CENTRE, ANCHOR_MAX, 0, Game_ClassicMode ? 80 : 25); + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -150); +} + +static void ClassicPauseScreen_Init(void* screen) { + struct PauseScreen* s = (struct PauseScreen*)screen; + static const struct SimpleButtonDesc descs[] = { + { 0, -100, "Options...", Menu_SwitchClassicOptions }, + { 0, -50, "Generate new level...", Menu_SwitchClassicGenLevel }, + { 0, 0, "Save level..", Menu_SwitchSaveLevel }, + { 0, 50, "Load level..", Menu_SwitchLoadLevel }, + { 0, 100, "Nostalgia options...", Menu_SwitchNostalgia } + }; + s->widgets = classicPause_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(classicPause_widgets); + s->descs = descs; + + /* Don't show nostalgia options in classic mode */ + s->descsCount = Game_ClassicMode ? 4 : 5; + PauseScreenBase_AddWidgets(s, 400); + s->maxVertices = Screen_CalcDefaultMaxVertices(s); + + if (Server.IsSinglePlayer) return; + s->btns[1].flags = WIDGET_FLAG_DISABLED; + s->btns[3].flags = WIDGET_FLAG_DISABLED; +} + +static const struct ScreenVTABLE ClassicPauseScreen_VTABLE = { + ClassicPauseScreen_Init, Screen_NullUpdate, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + ClassicPauseScreen_Layout, Screen_ContextLost, ClassicPauseScreen_ContextRecreated +}; +void ClassicPauseScreen_Show(void) { + struct PauseScreen* s = &PauseScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &ClassicPauseScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*--------------------------------------------------OptionsGroupScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static struct OptionsGroupScreen { + Screen_Body + struct FontDesc textFont; + struct ButtonWidget btns[8]; + struct TextWidget desc; + struct ButtonWidget done; +} OptionsGroupScreen; + +static struct Widget* optGroups_widgets[8 + 2]; + +static const char* const optsGroup_descs[8] = { + "&eMusic/Sound, view bobbing, and more", + "&eGui scale, font settings, and more", + "&eFPS limit, view distance, entity names/shadows", + "&eSet key and mouse bindings", + "&eChat options", + "&eHacks allowed, jump settings, and more", + "&eEnv colours, water level, weather, and more", + "&eSettings for resembling the original classic", +}; +static const struct SimpleButtonDesc optsGroup_btns[8] = { + { -160, -100, "Misc options...", Menu_SwitchMisc }, + { -160, -50, "Gui options...", Menu_SwitchGui }, + { -160, 0, "Graphics options...", Menu_SwitchGfx }, + { -160, 50, "Controls...", SwitchBindsMain }, + { 160, -100, "Chat options...", Menu_SwitchChat }, + { 160, -50, "Hacks settings...", Menu_SwitchHacks }, + { 160, 0, "Env settings...", Menu_SwitchEnv }, + { 160, 50, "Nostalgia options...", Menu_SwitchNostalgia } +}; + +static void OptionsGroupScreen_CheckHacksAllowed(void* screen) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + Widget_SetDisabled(&s->btns[6], + !Entities.CurPlayer->Hacks.CanAnyHacks); /* env settings */ + s->dirty = true; +} + +CC_NOINLINE static void OptionsGroupScreen_UpdateDesc(struct OptionsGroupScreen* s) { + TextWidget_SetConst(&s->desc, optsGroup_descs[s->selectedI], &s->textFont); +} + +static void OptionsGroupScreen_ContextLost(void* screen) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + Font_Free(&s->textFont); + Screen_ContextLost(screen); +} + +static void OptionsGroupScreen_ContextRecreated(void* screen) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + struct FontDesc titleFont; + Screen_UpdateVb(screen); + + Gui_MakeTitleFont(&titleFont); + Gui_MakeBodyFont(&s->textFont); + + Menu_SetButtons(s->btns, &titleFont, optsGroup_btns, 8); + ButtonWidget_SetConst(&s->done, "Done", &titleFont); + + if (s->selectedI >= 0) OptionsGroupScreen_UpdateDesc(s); + OptionsGroupScreen_CheckHacksAllowed(s); + Font_Free(&titleFont); +} + +static void OptionsGroupScreen_Layout(void* screen) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + Menu_LayoutButtons(s->btns, optsGroup_btns, 8); + Widget_SetLocation(&s->desc, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 100); + Menu_LayoutBack(&s->done); +} + +static void OptionsGroupScreen_Init(void* screen) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + + Event_Register_(&UserEvents.HackPermsChanged, s, OptionsGroupScreen_CheckHacksAllowed); + s->widgets = optGroups_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(optGroups_widgets); + s->selectedI = -1; + + Menu_AddButtons(s, s->btns, 300, optsGroup_btns, 8); + TextWidget_Add(s, &s->desc); + ButtonWidget_Add(s, &s->done, 400, Menu_SwitchPause); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static void OptionsGroupScreen_Free(void* screen) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + Event_Unregister_(&UserEvents.HackPermsChanged, s, OptionsGroupScreen_CheckHacksAllowed); +} + +static int OptionsGroupScreen_PointerMove(void* screen, int id, int x, int y) { + struct OptionsGroupScreen* s = (struct OptionsGroupScreen*)screen; + int i = Menu_DoPointerMove(s, id, x, y); + if (i == -1 || i == s->selectedI) return true; + if (i >= Array_Elems(optsGroup_descs)) return true; + + s->selectedI = i; + OptionsGroupScreen_UpdateDesc(s); + return true; +} + +static const struct ScreenVTABLE OptionsGroupScreen_VTABLE = { + OptionsGroupScreen_Init, Screen_NullUpdate, OptionsGroupScreen_Free, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, OptionsGroupScreen_PointerMove, Screen_TMouseScroll, + OptionsGroupScreen_Layout, OptionsGroupScreen_ContextLost, OptionsGroupScreen_ContextRecreated +}; +void OptionsGroupScreen_Show(void) { + struct OptionsGroupScreen* s = &OptionsGroupScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &OptionsGroupScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*----------------------------------------------------EditHotkeyScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static struct EditHotkeyScreen { + Screen_Body + struct HotkeyData curHotkey, origHotkey; + cc_bool supressNextPress; + int barX, barY[2], barWidth, barHeight; + struct FontDesc titleFont, textFont; + struct TextInputWidget input; + struct ButtonWidget btns[5], cancel; +} EditHotkeyScreen; + +static struct Widget* edithotkey_widgets[1 + 5 + 1]; + +static void HotkeyListScreen_MakeFlags(int flags, cc_string* str); +static void EditHotkeyScreen_MakeFlags(int flags, cc_string* str) { + if (flags == 0) String_AppendConst(str, " None"); + HotkeyListScreen_MakeFlags(flags, str); +} + +static void EditHotkeyScreen_UpdateBaseKey(struct EditHotkeyScreen* s) { + cc_string text; char textBuffer[STRING_SIZE]; + String_InitArray(text, textBuffer); + + if (s->selectedI == 0) { + String_AppendConst(&text, "Key: press a key.."); + } else { + String_AppendConst(&text, "Key: "); + String_AppendConst(&text, Input_DisplayNames[s->curHotkey.trigger]); + } + ButtonWidget_Set(&s->btns[0], &text, &s->titleFont); +} + +static void EditHotkeyScreen_UpdateModifiers(struct EditHotkeyScreen* s) { + cc_string text; char textBuffer[STRING_SIZE]; + String_InitArray(text, textBuffer); + + if (s->selectedI == 1) { + String_AppendConst(&text, "Modifiers: press a key.."); + } else { + String_AppendConst(&text, "Modifiers:"); + EditHotkeyScreen_MakeFlags(s->curHotkey.mods, &text); + } + ButtonWidget_Set(&s->btns[1], &text, &s->titleFont); +} + +static void EditHotkeyScreen_UpdateLeaveOpen(struct EditHotkeyScreen* s) { + cc_string text; char textBuffer[STRING_SIZE]; + String_InitArray(text, textBuffer); + + String_AppendConst(&text, "Input stays open: "); + String_AppendConst(&text, + (s->curHotkey.flags & HOTKEY_FLAG_STAYS_OPEN) ? "ON" : "OFF"); + ButtonWidget_Set(&s->btns[2], &text, &s->titleFont); +} + +static void EditHotkeyScreen_BaseKey(void* screen, void* b) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + s->selectedI = 0; + s->supressNextPress = true; + EditHotkeyScreen_UpdateBaseKey(s); +} + +static void EditHotkeyScreen_Modifiers(void* screen, void* b) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + s->selectedI = 1; + s->supressNextPress = true; + EditHotkeyScreen_UpdateModifiers(s); +} + +static void EditHotkeyScreen_LeaveOpen(void* screen, void* b) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + /* Reset 'waiting for key..' state of two other buttons */ + if (s->selectedI >= 0) { + s->selectedI = -1; + s->supressNextPress = false; + EditHotkeyScreen_UpdateBaseKey(s); + EditHotkeyScreen_UpdateModifiers(s); + } + + /* Toggle Input Stays Open flag */ + s->curHotkey.flags ^= HOTKEY_FLAG_STAYS_OPEN; + EditHotkeyScreen_UpdateLeaveOpen(s); +} + +static void EditHotkeyScreen_SaveChanges(void* screen, void* b) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + struct HotkeyData hk = s->origHotkey; + + if (hk.trigger) { + Hotkeys_Remove(hk.trigger, hk.mods); + StoredHotkeys_Remove(hk.trigger, hk.mods); + } + hk = s->curHotkey; + + if (hk.trigger) { + cc_string text = s->input.base.text; + cc_bool staysOpen = hk.flags & HOTKEY_FLAG_STAYS_OPEN; + + Hotkeys_Add(hk.trigger, hk.mods, &text, hk.flags); + StoredHotkeys_Add(hk.trigger, hk.mods, staysOpen, &text); + } + HotkeyListScreen_Show(); +} + +static void EditHotkeyScreen_RemoveHotkey(void* screen, void* b) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + struct HotkeyData hk = s->origHotkey; + + if (hk.trigger) { + Hotkeys_Remove(hk.trigger, hk.mods); + StoredHotkeys_Remove(hk.trigger, hk.mods); + } + HotkeyListScreen_Show(); +} + +static void EditHotkeyScreen_Render(void* screen, float delta) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + PackedCol grey = PackedCol_Make(150, 150, 150, 255); + + MenuScreen_Render2(screen, delta); + Gfx_Draw2DFlat(s->barX, s->barY[0], s->barWidth, s->barHeight, grey); + Gfx_Draw2DFlat(s->barX, s->barY[1], s->barWidth, s->barHeight, grey); +} + +static int EditHotkeyScreen_KeyPress(void* screen, char keyChar) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + if (s->supressNextPress) { + s->supressNextPress = false; + } else { + InputWidget_Append(&s->input.base, keyChar); + } + return true; +} + +static int EditHotkeyScreen_TextChanged(void* screen, const cc_string* str) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + InputWidget_SetText(&s->input.base, str); + return true; +} + +static int EditHotkeyScreen_KeyDown(void* screen, int key) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + if (s->selectedI >= 0) { + if (s->selectedI == 0) { + s->curHotkey.trigger = key; + } else if (s->selectedI == 1) { + if (key == CCKEY_LCTRL || key == CCKEY_RCTRL) s->curHotkey.mods |= HOTKEY_MOD_CTRL; + else if (key == CCKEY_LSHIFT || key == CCKEY_RSHIFT) s->curHotkey.mods |= HOTKEY_MOD_SHIFT; + else if (key == CCKEY_LALT || key == CCKEY_RALT) s->curHotkey.mods |= HOTKEY_MOD_ALT; + else s->curHotkey.mods = 0; + } + + s->supressNextPress = true; + s->selectedI = -1; + + EditHotkeyScreen_UpdateBaseKey(s); + EditHotkeyScreen_UpdateModifiers(s); + return true; + } + return Elem_HandlesKeyDown(&s->input.base, key) || Screen_InputDown(s, key); +} + +static void EditHotkeyScreen_ContextLost(void* screen) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + Font_Free(&s->titleFont); + Font_Free(&s->textFont); + Screen_ContextLost(screen); +} + +static void EditHotkeyScreen_ContextRecreated(void* screen) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + cc_bool existed = s->origHotkey.trigger != INPUT_NONE; + + Gui_MakeTitleFont(&s->titleFont); + Gui_MakeBodyFont(&s->textFont); + Screen_UpdateVb(screen); + + EditHotkeyScreen_UpdateBaseKey(s); + EditHotkeyScreen_UpdateModifiers(s); + EditHotkeyScreen_UpdateLeaveOpen(s); + + ButtonWidget_SetConst(&s->btns[3], existed ? "Save changes" : "Add hotkey", &s->titleFont); + ButtonWidget_SetConst(&s->btns[4], existed ? "Remove hotkey" : "Cancel", &s->titleFont); + TextInputWidget_SetFont(&s->input, &s->textFont); + ButtonWidget_SetConst(&s->cancel, "Cancel", &s->titleFont); +} + +static void EditHotkeyScreen_Update(void* screen, float delta) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + s->input.base.caretAccumulator += delta; +} + +static void EditHotkeyScreen_Layout(void* screen) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + s->barWidth = Display_ScaleX(500); + s->barX = Gui_CalcPos(ANCHOR_CENTRE, 0, s->barWidth, Window_UI.Width); + s->barHeight = Display_ScaleY(2); + + s->barY[0] = Gui_CalcPos(ANCHOR_CENTRE, Display_ScaleY(-65), + s->barHeight, Window_UI.Height); + s->barY[1] = Gui_CalcPos(ANCHOR_CENTRE, Display_ScaleY( 45), + s->barHeight, Window_UI.Height); + + Widget_SetLocation(&s->btns[0], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -150); + Widget_SetLocation(&s->btns[1], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -100); + Widget_SetLocation(&s->btns[2], ANCHOR_CENTRE, ANCHOR_CENTRE, -100, 10); + Widget_SetLocation(&s->btns[3], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 80); + Widget_SetLocation(&s->btns[4], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 130); + Widget_SetLocation(&s->input, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -35); + Menu_LayoutBack(&s->cancel); +} + +static void EditHotkeyScreen_Init(void* screen) { + struct EditHotkeyScreen* s = (struct EditHotkeyScreen*)screen; + struct MenuInputDesc desc; + cc_string text; + + s->widgets = edithotkey_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(edithotkey_widgets); + + s->selectedI = -1; + MenuInput_String(desc); + + ButtonWidget_Add(s, &s->btns[0], 300, EditHotkeyScreen_BaseKey); + ButtonWidget_Add(s, &s->btns[1], 300, EditHotkeyScreen_Modifiers); + ButtonWidget_Add(s, &s->btns[2], 300, EditHotkeyScreen_LeaveOpen); + ButtonWidget_Add(s, &s->btns[3], 300, EditHotkeyScreen_SaveChanges); + ButtonWidget_Add(s, &s->btns[4], 300, EditHotkeyScreen_RemoveHotkey); + + if (s->origHotkey.trigger) { + text = StringsBuffer_UNSAFE_Get(&HotkeysText, s->origHotkey.textIndex); + } else { text = String_Empty; } + + TextInputWidget_Add(s, &s->input, 500, &text, &desc); + ButtonWidget_Add(s, &s->cancel, 400, Menu_SwitchHotkeys); + s->input.onscreenPlaceholder = "Hotkey text"; + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE EditHotkeyScreen_VTABLE = { + EditHotkeyScreen_Init, EditHotkeyScreen_Update, Menu_CloseKeyboard, + EditHotkeyScreen_Render, Screen_BuildMesh, + EditHotkeyScreen_KeyDown, Screen_InputUp, EditHotkeyScreen_KeyPress, EditHotkeyScreen_TextChanged, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + EditHotkeyScreen_Layout, EditHotkeyScreen_ContextLost, EditHotkeyScreen_ContextRecreated +}; +void EditHotkeyScreen_Show(struct HotkeyData original) { + struct EditHotkeyScreen* s = &EditHotkeyScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &EditHotkeyScreen_VTABLE; + s->origHotkey = original; + s->curHotkey = original; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*-----------------------------------------------------GenLevelScreen------------------------------------------------------* +*#########################################################################################################################*/ +static struct GenLevelScreen { + Screen_Body + struct FontDesc textFont; + struct ButtonWidget flatgrass, vanilla, cancel; + struct TextInputWidget inputs[4]; + struct TextWidget labels[4], title; +} GenLevelScreen; +#define GENLEVEL_NUM_INPUTS 4 + +static struct Widget* gen_widgets[2 * GENLEVEL_NUM_INPUTS + 4]; + +CC_NOINLINE static int GenLevelScreen_GetInt(struct GenLevelScreen* s, int index) { + struct TextInputWidget* input = &s->inputs[index]; + struct MenuInputDesc* desc; + cc_string text = input->base.text; + int value; + + desc = &input->desc; + if (!desc->VTABLE->IsValidValue(desc, &text)) return 0; + Convert_ParseInt(&text, &value); return value; +} + +CC_NOINLINE static int GenLevelScreen_GetSeedInt(struct GenLevelScreen* s, int index) { + struct TextInputWidget* input = &s->inputs[index]; + RNGState rnd; + + if (!input->base.text.length) { + Random_SeedFromCurrentTime(&rnd); + return Random_Next(&rnd, Int32_MaxValue); + } + return GenLevelScreen_GetInt(s, index); +} + +static void GenLevelScreen_Gen(void* screen, const struct MapGenerator* gen) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + int width = GenLevelScreen_GetInt(s, 0); + int height = GenLevelScreen_GetInt(s, 1); + int length = GenLevelScreen_GetInt(s, 2); + int seed = GenLevelScreen_GetSeedInt(s, 3); + + cc_uint64 volume = (cc_uint64)width * height * length; + if (volume > Int32_MaxValue) { + Chat_AddRaw("&cThe generated map's volume is too big."); + } else if (!width || !height || !length) { + Chat_AddRaw("&cOne of the map dimensions is invalid."); + } else { + Gen_Active = gen; + Gen_Seed = seed; + Gui_Remove((struct Screen*)s); + Menu_BeginGen(width, height, length); + } +} + +static void GenLevelScreen_Flatgrass(void* a, void* b) { GenLevelScreen_Gen(a, &FlatgrassGen); } +static void GenLevelScreen_Notchy(void* a, void* b) { GenLevelScreen_Gen(a, &NotchyGen); } + +static void GenLevelScreen_Make(struct GenLevelScreen* s, int i, int def) { + cc_string tmp; char tmpBuffer[STRING_SIZE]; + struct MenuInputDesc desc; + + if (i == 3) { + MenuInput_Seed(desc); + } else { + MenuInput_Int(desc, 1, 8192, def); + } + + String_InitArray(tmp, tmpBuffer); + desc.VTABLE->GetDefault(&desc, &tmp); + + TextWidget_Add(s, &s->labels[i]); + s->labels[i].color = PackedCol_Make(224, 224, 224, 255); + + /* TODO placeholder */ + TextInputWidget_Add(s, &s->inputs[i], 200, &tmp, &desc); + s->inputs[i].base.showCaret = false; + s->inputs[i].onscreenType = KEYBOARD_TYPE_INTEGER; + s->inputs[i].base.meta.val = 10000; +} +#define GenLevelScreen_IsInput(w) (w)->meta.val == 10000 + +static struct TextInputWidget* GenLevelScreen_SelectedInput(struct GenLevelScreen* s) { + if (s->selectedI >= 0 && GenLevelScreen_IsInput(s->widgets[s->selectedI])) { + return (struct TextInputWidget*)s->widgets[s->selectedI]; + } + return NULL; +} + +static int GenLevelScreen_KeyDown(void* screen, int key) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + struct TextInputWidget* selected = GenLevelScreen_SelectedInput(s); + struct MenuInputDesc* desc; + + if (selected) { + if (Elem_HandlesKeyDown(&selected->base, key)) return true; + + desc = &selected->desc; + if (desc->VTABLE->ProcessInput(desc, &selected->base.text, key)) return true; + } + return Menu_InputDown(s, key); +} + +static int GenLevelScreen_KeyPress(void* screen, char keyChar) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + struct TextInputWidget* selected = GenLevelScreen_SelectedInput(s); + + if (selected) InputWidget_Append(&selected->base, keyChar); + return true; +} + +static int GenLevelScreen_TextChanged(void* screen, const cc_string* str) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + struct TextInputWidget* selected = GenLevelScreen_SelectedInput(s); + + if (selected) InputWidget_SetText(&selected->base, str); + return true; +} + +static int GenLevelScreen_PointerDown(void* screen, int id, int x, int y) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + struct TextInputWidget* selected; + s->selectedI = Screen_DoPointerDown(screen, id, x, y); + + selected = GenLevelScreen_SelectedInput(s); + if (selected) OnscreenKeyboard_SetText(&selected->base.text); + return TOUCH_TYPE_GUI; +} + +static void GenLevelScreen_ContextLost(void* screen) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + Font_Free(&s->textFont); + Screen_ContextLost(screen); +} + +static void GenLevelScreen_ContextRecreated(void* screen) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + struct FontDesc titleFont; + Gui_MakeTitleFont(&titleFont); + Gui_MakeBodyFont(&s->textFont); + Screen_UpdateVb(screen); + + TextInputWidget_SetFont(&s->inputs[0], &s->textFont); + TextInputWidget_SetFont(&s->inputs[1], &s->textFont); + TextInputWidget_SetFont(&s->inputs[2], &s->textFont); + TextInputWidget_SetFont(&s->inputs[3], &s->textFont); + + TextWidget_SetConst(&s->labels[0], "Width:", &s->textFont); + TextWidget_SetConst(&s->labels[1], "Height:", &s->textFont); + TextWidget_SetConst(&s->labels[2], "Length:", &s->textFont); + TextWidget_SetConst(&s->labels[3], "Seed:", &s->textFont); + + TextWidget_SetConst(&s->title, "Generate new level", &s->textFont); + ButtonWidget_SetConst(&s->flatgrass, "Flatgrass", &titleFont); + ButtonWidget_SetConst(&s->vanilla, "Vanilla", &titleFont); + ButtonWidget_SetConst(&s->cancel, "Cancel", &titleFont); + Font_Free(&titleFont); +} + +static void GenLevelScreen_Update(void* screen, float delta) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + struct TextInputWidget* selected = GenLevelScreen_SelectedInput(s); + int i; + + for (i = 0; i < GENLEVEL_NUM_INPUTS; i++) + { + s->inputs[i].base.showCaret = &s->inputs[i] == selected; + } + if (selected) selected->base.caretAccumulator += delta; +} + +static void GenLevelScreen_Layout(void* screen) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + int i, y; + for (i = 0; i < 4; i++) { + y = (i - 2) * 40; + Widget_SetLocation(&s->inputs[i], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, y); + Widget_SetLocation(&s->labels[i], ANCHOR_CENTRE_MAX, ANCHOR_CENTRE, 110, y); + } + + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -130); + Widget_SetLocation(&s->flatgrass, ANCHOR_CENTRE, ANCHOR_CENTRE, -120, 100); + Widget_SetLocation(&s->vanilla, ANCHOR_CENTRE, ANCHOR_CENTRE, 120, 100); + Menu_LayoutBack(&s->cancel); +} + +static void GenLevelScreen_Init(void* screen) { + struct GenLevelScreen* s = (struct GenLevelScreen*)screen; + s->widgets = gen_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(gen_widgets); + s->selectedI = -1; + + GenLevelScreen_Make(s, 0, World.Width); + GenLevelScreen_Make(s, 1, World.Height); + GenLevelScreen_Make(s, 2, World.Length); + GenLevelScreen_Make(s, 3, 0); + + TextWidget_Add(s, &s->title); + ButtonWidget_Add(s, &s->flatgrass, 200, GenLevelScreen_Flatgrass); + ButtonWidget_Add(s, &s->vanilla, 200, GenLevelScreen_Notchy); + ButtonWidget_Add(s, &s->cancel, 400, Menu_SwitchPause); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE GenLevelScreen_VTABLE = { + GenLevelScreen_Init, GenLevelScreen_Update, Menu_CloseKeyboard, + MenuScreen_Render2, Screen_BuildMesh, + GenLevelScreen_KeyDown, Screen_InputUp, GenLevelScreen_KeyPress, GenLevelScreen_TextChanged, + GenLevelScreen_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + GenLevelScreen_Layout, GenLevelScreen_ContextLost, GenLevelScreen_ContextRecreated +}; +void GenLevelScreen_Show(void) { + struct GenLevelScreen* s = &GenLevelScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &GenLevelScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*----------------------------------------------------ClassicGenScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static struct ClassicGenScreen { + Screen_Body + struct ButtonWidget btns[3], cancel; + struct TextWidget title; +} ClassicGenScreen; + +static struct Widget* classicgen_widgets[1 + 3 + 1]; + +static void ClassicGenScreen_Gen(int size) { + RNGState rnd; Random_SeedFromCurrentTime(&rnd); + Gen_Active = &NotchyGen; + Gen_Seed = Random_Next(&rnd, Int32_MaxValue); + + Gui_Remove((struct Screen*)&ClassicGenScreen); + Menu_BeginGen(size, 64, size); +} + +static void ClassicGenScreen_Small(void* a, void* b) { ClassicGenScreen_Gen(128); } +static void ClassicGenScreen_Medium(void* a, void* b) { ClassicGenScreen_Gen(256); } +static void ClassicGenScreen_Huge(void* a, void* b) { ClassicGenScreen_Gen(512); } + +static void ClassicGenScreen_ContextRecreated(void* screen) { + struct ClassicGenScreen* s = (struct ClassicGenScreen*)screen; + struct FontDesc titleFont; + + Screen_UpdateVb(screen); + Gui_MakeTitleFont(&titleFont); + TextWidget_SetConst(&s->title, "Generate new level", &titleFont); + + ButtonWidget_SetConst(&s->btns[0], "Small", &titleFont); + ButtonWidget_SetConst(&s->btns[1], "Normal", &titleFont); + ButtonWidget_SetConst(&s->btns[2], "Huge", &titleFont); + ButtonWidget_SetConst(&s->cancel, "Cancel", &titleFont); + Font_Free(&titleFont); +} + +static void ClassicGenScreen_Layout(void* screen) { + struct ClassicGenScreen* s = (struct ClassicGenScreen*)screen; + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -150); + Widget_SetLocation(&s->btns[0], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -100); + Widget_SetLocation(&s->btns[1], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -50); + Widget_SetLocation(&s->btns[2], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 0); + Widget_SetLocation(&s->cancel, ANCHOR_CENTRE, ANCHOR_MAX, 0, 80); +} + +static void ClassicGenScreen_Init(void* screen) { + struct ClassicGenScreen* s = (struct ClassicGenScreen*)screen; + s->widgets = classicgen_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(classicgen_widgets); + + TextWidget_Add(s, &s->title); + ButtonWidget_Add(s, &s->btns[0], 400, ClassicGenScreen_Small); + ButtonWidget_Add(s, &s->btns[1], 400, ClassicGenScreen_Medium); + ButtonWidget_Add(s, &s->btns[2], 400, ClassicGenScreen_Huge); + ButtonWidget_Add(s, &s->cancel, 400, Menu_SwitchPause); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE ClassicGenScreen_VTABLE = { + ClassicGenScreen_Init, Screen_NullUpdate, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + ClassicGenScreen_Layout, Screen_ContextLost, ClassicGenScreen_ContextRecreated +}; +void ClassicGenScreen_Show(void) { + struct ClassicGenScreen* s = &ClassicGenScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &ClassicGenScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*----------------------------------------------------SaveLevelScreen------------------------------------------------------* +*#########################################################################################################################*/ +static struct SaveLevelScreen { + Screen_Body + struct FontDesc titleFont, textFont; + struct ButtonWidget save, file, cancel; + struct TextInputWidget input; + struct TextWidget desc; +} SaveLevelScreen; + +static struct Widget* save_widgets[3 + 1 + 1]; + +static void SaveLevelScreen_UpdateSave(struct SaveLevelScreen* s) { + ButtonWidget_SetConst(&s->save, + s->save.optName ? "&cOverwrite existing?" : "Save", &s->titleFont); +} + +static void SaveLevelScreen_RemoveOverwrites(struct SaveLevelScreen* s) { + if (s->save.optName) { + s->save.optName = NULL; + SaveLevelScreen_UpdateSave(s); + } +} + +static cc_result SaveLevelScreen_SaveMap(const cc_string* path) { + static const cc_string schematic = String_FromConst(".schematic"); + static const cc_string mine = String_FromConst(".mine"); + struct Stream stream, compStream; + struct GZipState state; + cc_result res; + + res = Stream_CreateFile(&stream, path); + if (res) { Logger_SysWarn2(res, "creating", path); return res; } + GZip_MakeStream(&compStream, &state, &stream); + + if (String_CaselessEnds(path, &schematic)) { + res = Schematic_Save(&compStream); + } else if (String_CaselessEnds(path, &mine)) { + res = Dat_Save(&compStream); + } else { + res = Cw_Save(&compStream); + } + + if (res) { + stream.Close(&stream); + Logger_SysWarn2(res, "encoding", path); return res; + } + + if ((res = compStream.Close(&compStream))) { + stream.Close(&stream); + Logger_SysWarn2(res, "closing", path); return res; + } + + res = stream.Close(&stream); + if (res) { Logger_SysWarn2(res, "closing", path); return res; } + + World.LastSave = Game.Time; + Gui_ShowPauseMenu(); + return 0; +} + +static void SaveLevelScreen_Save(void* screen, void* widget) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + struct ButtonWidget* btn = (struct ButtonWidget*)widget; + cc_string path; char pathBuffer[FILENAME_SIZE]; + cc_string file = s->input.base.text; + cc_result res; + + if (!file.length) { + TextWidget_SetConst(&s->desc, "&ePlease enter a filename", &s->textFont); + return; + } + + String_InitArray(path, pathBuffer); + String_Format1(&path, "maps/%s.cw", &file); + String_Copy(&World.Name, &file); + + if (File_Exists(&path) && !btn->optName) { + btn->optName = ""; + SaveLevelScreen_UpdateSave(s); + return; + } + + SaveLevelScreen_RemoveOverwrites(s); + if ((res = SaveLevelScreen_SaveMap(&path))) return; + Chat_Add1("&eSaved map to: %s", &path); +} + +static void SaveLevelScreen_UploadCallback(const cc_string* path) { + cc_result res = SaveLevelScreen_SaveMap(path); + if (!res) Chat_Add1("&eSaved map to: %s", path); +} + +static void SaveLevelScreen_File(void* screen, void* b) { + static const char* const titles[] = { + "ClassiCube map", "Minecraft schematic", "Minecraft classic map", NULL + }; + static const char* const filters[] = { + ".cw", ".schematic", ".mine", NULL + }; + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + struct SaveFileDialogArgs args; + cc_result res; + + args.filters = filters; + args.titles = titles; + args.defaultName = s->input.base.text; + args.Callback = SaveLevelScreen_UploadCallback; + + res = Window_SaveFileDialog(&args); + if (res == SFD_ERR_NEED_DEFAULT_NAME) { + TextWidget_SetConst(&s->desc, "&ePlease enter a filename", &s->textFont); + } else if (res) { + Logger_SimpleWarn(res, "showing save file dialog"); + } +} + +static int SaveLevelScreen_KeyPress(void* screen, char keyChar) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + SaveLevelScreen_RemoveOverwrites(s); + InputWidget_Append(&s->input.base, keyChar); + return true; +} + +static int SaveLevelScreen_TextChanged(void* screen, const cc_string* str) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + SaveLevelScreen_RemoveOverwrites(s); + InputWidget_SetText(&s->input.base, str); + return true; +} + +static int SaveLevelScreen_KeyDown(void* screen, int key) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + if (Elem_HandlesKeyDown(&s->input.base, key)) { + SaveLevelScreen_RemoveOverwrites(s); + return true; + } + return Menu_InputDown(s, key); +} + +static void SaveLevelScreen_ContextLost(void* screen) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + Font_Free(&s->titleFont); + Font_Free(&s->textFont); + Screen_ContextLost(screen); +} + +static void SaveLevelScreen_ContextRecreated(void* screen) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + Gui_MakeTitleFont(&s->titleFont); + Gui_MakeBodyFont(&s->textFont); + + Screen_UpdateVb(screen); + SaveLevelScreen_UpdateSave(s); + + TextInputWidget_SetFont(&s->input, &s->textFont); + ButtonWidget_SetConst(&s->cancel, "Cancel", &s->titleFont); +#ifdef CC_BUILD_WEB + ButtonWidget_SetConst(&s->file, "Download", &s->titleFont); +#else + ButtonWidget_SetConst(&s->file, "Save file...", &s->titleFont); +#endif +} + +static void SaveLevelScreen_Update(void* screen, float delta) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + s->input.base.caretAccumulator += delta; +} + +static void SaveLevelScreen_Layout(void* screen) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + Widget_SetLocation(&s->input, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -30); + Widget_SetLocation(&s->save, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 20); + Widget_SetLocation(&s->desc, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 65); + + Widget_SetLocation(&s->file, ANCHOR_CENTRE, ANCHOR_MAX, 0, 70); + Menu_LayoutBack(&s->cancel); +} + +static void SaveLevelScreen_Init(void* screen) { + struct SaveLevelScreen* s = (struct SaveLevelScreen*)screen; + struct MenuInputDesc desc; + + s->widgets = save_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(save_widgets); + MenuInput_Path(desc); + + ButtonWidget_Add(s, &s->save, 400, SaveLevelScreen_Save); + ButtonWidget_Add(s, &s->file, 400, SaveLevelScreen_File); + + ButtonWidget_Add(s, &s->cancel, 400, Menu_SwitchPause); + TextInputWidget_Add(s, &s->input, 400, &World.Name, &desc); + TextWidget_Add(s, &s->desc); + s->input.onscreenPlaceholder = "Map name"; + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE SaveLevelScreen_VTABLE = { + SaveLevelScreen_Init, SaveLevelScreen_Update, Menu_CloseKeyboard, + MenuScreen_Render2, Screen_BuildMesh, + SaveLevelScreen_KeyDown, Screen_InputUp, SaveLevelScreen_KeyPress, SaveLevelScreen_TextChanged, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + SaveLevelScreen_Layout, SaveLevelScreen_ContextLost, SaveLevelScreen_ContextRecreated +}; +void SaveLevelScreen_Show(void) { + struct SaveLevelScreen* s = &SaveLevelScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &SaveLevelScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*---------------------------------------------------TexturePackScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static void TexturePackScreen_EntryClick(void* screen, void* widget) { + struct ListScreen* s = (struct ListScreen*)screen; + cc_string file = ListScreen_UNSAFE_GetCur(s, widget); + cc_result res; + + TexturePack_SetDefault(&file); + TexturePack_Url.length = 0; + res = TexturePack_ExtractCurrent(true); + + /* FileNotFound error may be because user deleted .zips from disc */ + if (res != ReturnCode_FileNotFound) return; + Chat_AddRaw("&eReloading texture pack list as it may be out of date"); + ListScreen_Reload(s); +} + +static void TexturePackScreen_FilterFiles(const cc_string* path, void* obj) { + static const cc_string zip = String_FromConst(".zip"); + cc_string relPath = *path; + if (!String_CaselessEnds(path, &zip)) return; + + Utils_UNSAFE_TrimFirstDirectory(&relPath); + StringsBuffer_Add((struct StringsBuffer*)obj, &relPath); +} + +static void TexturePackScreen_LoadEntries(struct ListScreen* s) { + static const cc_string path = String_FromConst("texpacks"); + Directory_Enum(&path, &s->entries, TexturePackScreen_FilterFiles); + StringsBuffer_Sort(&s->entries); +} + +static void TexturePackScreen_UploadCallback(const cc_string* path) { +#ifdef CC_BUILD_WEB + cc_string relPath = *path; + Utils_UNSAFE_GetFilename(&relPath); + + ListScreen_Reload(&ListScreen); + TexturePack_SetDefault(&relPath); +#else + String_Copy(&TexturePack_Path, path); +#endif + TexturePack_ExtractCurrent(true); +} + +static void TexturePackScreen_ActionFunc(void* s, void* w) { + static const char* const filters[] = { + ".zip", NULL + }; + static struct OpenFileDialogArgs args = { + "Texture packs", filters, + TexturePackScreen_UploadCallback, + OFD_UPLOAD_PERSIST, "texpacks" + }; + + cc_result res = Window_OpenFileDialog(&args); + if (res) Logger_SimpleWarn(res, "showing open file dialog"); +} + +void TexturePackScreen_Show(void) { + struct ListScreen* s = &ListScreen; + s->titleText = "Select a texture pack"; +#ifdef CC_BUILD_WEB + s->actionText = "Upload"; +#else + s->actionText = "Load file..."; +#endif + + s->ActionClick = TexturePackScreen_ActionFunc; + s->LoadEntries = TexturePackScreen_LoadEntries; + s->EntryClick = TexturePackScreen_EntryClick; + s->DoneClick = Menu_SwitchPause; + s->UpdateEntry = ListScreen_UpdateEntry; + ListScreen_Show(); +} + + +/*########################################################################################################################* +*----------------------------------------------------FontListScreen-------------------------------------------------------* +*#########################################################################################################################*/ +static void FontListScreen_EntryClick(void* screen, void* widget) { + struct ListScreen* s = (struct ListScreen*)screen; + cc_string fontName = ListScreen_UNSAFE_GetCur(s, widget); + + Options_Set(OPT_FONT_NAME, &fontName); + SysFont_SetDefault(&fontName); +} + +static void FontListScreen_UpdateEntry(struct ListScreen* s, struct ButtonWidget* button, const cc_string* text) { + struct FontDesc font; + cc_result res; + + if (String_CaselessEqualsConst(text, LISTSCREEN_EMPTY)) { + ButtonWidget_Set(button, text, &s->font); return; + } + res = SysFont_Make(&font, text, 16, FONT_FLAGS_NONE); + + if (!res) { + ButtonWidget_Set(button, text, &font); + } else { + Logger_SimpleWarn2(res, "making font", text); + ButtonWidget_Set(button, text, &s->font); + } + Font_Free(&font); +} + +static void FontListScreen_LoadEntries(struct ListScreen* s) { + SysFonts_GetNames(&s->entries); + ListScreen_Select(s, SysFonts_UNSAFE_GetDefault()); +} + +static void FontListScreen_RegisterCallback(const cc_string* path) { + Chat_Add1("Loaded font from %s", path); +} + +static void FontListScreen_UploadCallback(const cc_string* path) { + cc_result res = SysFonts_Register(path, FontListScreen_RegisterCallback); + + if (res) { + Logger_SimpleWarn2(res, "loading font from", path); + } else { + SysFonts_SaveCache(); + } +} + +static void FontListScreen_ActionFunc(void* s, void* w) { + static const char* const filters[] = { + ".ttf", ".otf", NULL + }; + static struct OpenFileDialogArgs args = { + "Font files", filters, + FontListScreen_UploadCallback, + OFD_UPLOAD_DELETE, "tmp" + }; + + cc_result res = Window_OpenFileDialog(&args); + if (res) Logger_SimpleWarn(res, "showing open file dialog"); +} + +void FontListScreen_Show(void) { + struct ListScreen* s = &ListScreen; + s->titleText = "Select a font"; + s->actionText = "Load font..."; + s->ActionClick = FontListScreen_ActionFunc; + + s->LoadEntries = FontListScreen_LoadEntries; + s->EntryClick = FontListScreen_EntryClick; + s->DoneClick = Menu_SwitchGui; + s->UpdateEntry = FontListScreen_UpdateEntry; + ListScreen_Show(); +} + + +/*########################################################################################################################* +*---------------------------------------------------HotkeyListScreen------------------------------------------------------* +*#########################################################################################################################*/ +/* TODO: Hotkey added event for CPE */ +static void HotkeyListScreen_EntryClick(void* screen, void* widget) { + struct ListScreen* s = (struct ListScreen*)screen; + struct HotkeyData h, original = { 0 }; + cc_string text, key, value; + int trigger; + int i, mods = 0; + + text = ListScreen_UNSAFE_GetCur(s, widget); + + String_UNSAFE_Separate(&text, '+', &key, &value); + if (String_ContainsConst(&value, "Ctrl")) mods |= HOTKEY_MOD_CTRL; + if (String_ContainsConst(&value, "Shift")) mods |= HOTKEY_MOD_SHIFT; + if (String_ContainsConst(&value, "Alt")) mods |= HOTKEY_MOD_ALT; + + trigger = Utils_ParseEnum(&key, INPUT_NONE, Input_DisplayNames, INPUT_COUNT); + for (i = 0; i < HotkeysText.count; i++) { + h = HotkeysList[i]; + if (h.trigger == trigger && h.mods == mods) { original = h; break; } + } + + EditHotkeyScreen_Show(original); +} + +static void HotkeyListScreen_MakeFlags(int flags, cc_string* str) { + if (flags & HOTKEY_MOD_CTRL) String_AppendConst(str, " Ctrl"); + if (flags & HOTKEY_MOD_SHIFT) String_AppendConst(str, " Shift"); + if (flags & HOTKEY_MOD_ALT) String_AppendConst(str, " Alt"); +} + +static void HotkeyListScreen_LoadEntries(struct ListScreen* s) { + cc_string text; char textBuffer[STRING_SIZE]; + struct HotkeyData hKey; + int i; + String_InitArray(text, textBuffer); + + for (i = 0; i < HotkeysText.count; i++) { + hKey = HotkeysList[i]; + text.length = 0; + String_AppendConst(&text, Input_DisplayNames[hKey.trigger]); + + if (hKey.mods) { + String_AppendConst(&text, " +"); + HotkeyListScreen_MakeFlags(hKey.mods, &text); + } + StringsBuffer_Add(&s->entries, &text); + } + StringsBuffer_Sort(&s->entries); +} + +static void HotkeyListScreen_UpdateEntry(struct ListScreen* s, struct ButtonWidget* button, const cc_string* text) { + if (text->length) ButtonWidget_Set(button, text, &s->font); +} + +static void HotkeyListScreen_ActionFunc(void* s, void* w) { + struct HotkeyData original = { 0 }; + EditHotkeyScreen_Show(original); +} + +void HotkeyListScreen_Show(void) { + struct ListScreen* s = &ListScreen; + s->titleText = "Modify hotkeys"; + s->actionText = "New hotkey..."; + + s->ActionClick = HotkeyListScreen_ActionFunc; + s->LoadEntries = HotkeyListScreen_LoadEntries; + s->EntryClick = HotkeyListScreen_EntryClick; + s->DoneClick = Menu_SwitchPause; + s->UpdateEntry = HotkeyListScreen_UpdateEntry; + ListScreen_Show(); +} + + +/*########################################################################################################################* +*----------------------------------------------------LoadLevelScreen------------------------------------------------------* +*#########################################################################################################################*/ +static void LoadLevelScreen_EntryClick(void* screen, void* widget) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + struct ListScreen* s = (struct ListScreen*)screen; + cc_result res; + + cc_string relPath = ListScreen_UNSAFE_GetCur(s, widget); + String_InitArray(path, pathBuffer); + String_Format1(&path, "maps/%s", &relPath); + res = Map_LoadFrom(&path); + + /* FileNotFound error may be because user deleted maps from disc */ + if (res != ReturnCode_FileNotFound) return; + Chat_AddRaw("&eReloading level list as it may be out of date"); + ListScreen_Reload(s); +} + +static void LoadLevelScreen_FilterFiles(const cc_string* path, void* obj) { + struct MapImporter* imp = MapImporter_Find(path); + cc_string relPath = *path; + if (!imp) return; + + Utils_UNSAFE_TrimFirstDirectory(&relPath); + StringsBuffer_Add((struct StringsBuffer*)obj, &relPath); +} + +static void LoadLevelScreen_LoadEntries(struct ListScreen* s) { + static const cc_string path = String_FromConst("maps"); + Directory_Enum(&path, &s->entries, LoadLevelScreen_FilterFiles); + StringsBuffer_Sort(&s->entries); +} + +static void LoadLevelScreen_UploadCallback(const cc_string* path) { Map_LoadFrom(path); } +static void LoadLevelScreen_ActionFunc(void* s, void* w) { + static const char* const filters[] = { + ".cw", ".dat", ".lvl", ".mine", ".fcm", ".mclevel", NULL + }; /* TODO not hardcode list */ + static struct OpenFileDialogArgs args = { + "Classic map files", filters, + LoadLevelScreen_UploadCallback, + OFD_UPLOAD_DELETE, "tmp" + }; + + cc_result res = Window_OpenFileDialog(&args); + if (res) Logger_SimpleWarn(res, "showing open file dialog"); +} + +void LoadLevelScreen_Show(void) { + struct ListScreen* s = &ListScreen; + s->titleText = "Load level"; +#ifdef CC_BUILD_WEB + s->actionText = "Upload"; +#else + s->actionText = "Load file..."; +#endif + + s->ActionClick = LoadLevelScreen_ActionFunc; + s->LoadEntries = LoadLevelScreen_LoadEntries; + s->EntryClick = LoadLevelScreen_EntryClick; + s->DoneClick = Menu_SwitchPause; + s->UpdateEntry = ListScreen_UpdateEntry; + ListScreen_Show(); +} + + +/*########################################################################################################################* +*----------------------------------------------------BindSourcesScreen----------------------------------------------------* +*#########################################################################################################################*/ +static struct BindsSourceScreen { + Screen_Body + struct ButtonWidget btns[2], cancel; +} BindsSourceScreen; +static int binds_gamepad; /* Default to Normal (Keyboard/Mouse) */ + +static struct Widget* bindsSource_widgets[3]; + +static void BindsSourceScreen_ModeNormal(void* screen, void* b) { + binds_gamepad = false; + NormalBindingsScreen_Show(); +} + +static void BindsSourceScreen_ModeGamepad(void* screen, void* b) { + binds_gamepad = true; + NormalBindingsScreen_Show(); +} + +static void BindsSourceScreen_ContextRecreated(void* screen) { + struct BindsSourceScreen* s = (struct BindsSourceScreen*)screen; + struct FontDesc font; + Gui_MakeTitleFont(&font); + Screen_UpdateVb(screen); + + ButtonWidget_SetConst(&s->btns[0], "Keyboard/Mouse", &font); + ButtonWidget_SetConst(&s->btns[1], "Gamepad/Controller", &font); + ButtonWidget_SetConst(&s->cancel, "Cancel", &font); + Font_Free(&font); +} + +static void BindsSourceScreen_Layout(void* screen) { + struct BindsSourceScreen* s = (struct BindsSourceScreen*)screen; + Widget_SetLocation(&s->btns[0], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -25); + Widget_SetLocation(&s->btns[1], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 25); + Menu_LayoutBack(&s->cancel); +} + +static void BindsSourceScreen_Init(void* screen) { + struct BindsSourceScreen* s = (struct BindsSourceScreen*)screen; + + s->widgets = bindsSource_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(bindsSource_widgets); + s->selectedI = -1; + + ButtonWidget_Add(s, &s->btns[0], 300, BindsSourceScreen_ModeNormal); + ButtonWidget_Add(s, &s->btns[1], 300, BindsSourceScreen_ModeGamepad); + ButtonWidget_Add(s, &s->cancel, 400, Menu_SwitchPause); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE BindsSourceScreen_VTABLE = { + BindsSourceScreen_Init, Screen_NullUpdate, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + BindsSourceScreen_Layout, Screen_ContextLost, BindsSourceScreen_ContextRecreated +}; +void BindsSourceScreen_Show(void) { + struct BindsSourceScreen* s = &BindsSourceScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &BindsSourceScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + +static void SwitchBindsMain(void* s, void* w) { + if (Input.Sources == (INPUT_SOURCE_NORMAL | INPUT_SOURCE_GAMEPAD)) { + /* User needs to decide whether to configure mouse/keyboard or gamepad */ + BindsSourceScreen_Show(); + } else if (Input.Sources == INPUT_SOURCE_GAMEPAD) { + binds_gamepad = true; + NormalBindingsScreen_Show(); + } else { + binds_gamepad = false; + NormalBindingsScreen_Show(); + } +} + + +/*########################################################################################################################* +*---------------------------------------------------KeyBindsScreen-----------------------------------------------------* +*#########################################################################################################################*/ +struct KeyBindsScreen; +typedef void (*InitKeyBindings)(struct KeyBindsScreen* s); +#define KEYBINDS_MAX_BTNS 12 + +static struct KeyBindsScreen { + Screen_Body + int curI, bindsCount; + const char* const* descs; + const cc_uint8* binds; + Widget_LeftClick leftPage, rightPage; + int btnWidth, topY, arrowsY, leftLen; + const char* titleText; + const char* msgText; + struct FontDesc titleFont; + struct TextWidget title, msg; + struct ButtonWidget back, left, right; + struct ButtonWidget buttons[KEYBINDS_MAX_BTNS]; +} KeyBindsScreen; + +static struct Widget* key_widgets[KEYBINDS_MAX_BTNS + 5]; + +static BindMapping KeyBindsScreen_GetBinding(struct KeyBindsScreen* s, int i) { + const BindMapping* curBinds; + + curBinds = binds_gamepad ? PadBind_Mappings : KeyBind_Mappings; + return curBinds[s->binds[i]]; +} + +static void KeyBindsScreen_Update(struct KeyBindsScreen* s, int i) { + cc_string text; char textBuffer[STRING_SIZE]; + BindMapping curBind; + + String_InitArray(text, textBuffer); + curBind = KeyBindsScreen_GetBinding(s, i); + + String_Format4(&text, s->curI == i ? "> %c: %c%c%c <" : "%c: %c%c%c", + s->descs[i], + Input_DisplayNames[curBind.button1], + curBind.button2 ? " + " : "", + curBind.button2 ? Input_DisplayNames[curBind.button2] : ""); + + ButtonWidget_Set(&s->buttons[i], &text, &s->titleFont); + s->dirty = true; +} + +static void KeyBindsScreen_OnBindingClick(void* screen, void* widget) { + struct KeyBindsScreen* s = (struct KeyBindsScreen*)screen; + struct ButtonWidget* btn = (struct ButtonWidget*)widget; + + int old = s->curI; + s->curI = btn->meta.val; + s->closable = false; + + KeyBindsScreen_Update(s, s->curI); + /* previously selected a different button for binding */ + if (old >= 0) KeyBindsScreen_Update(s, old); +} + +static void KeyBindsScreen_ResetBinding(InputBind bind) { + if (binds_gamepad) { + PadBind_Reset(bind); + } else { + KeyBind_Reset(bind); + } +} + +static void KeyBindsScreen_UpdateBinding(InputBind bind, int key) { + if (binds_gamepad) { + PadBind_Set(bind, key); + } else { + KeyBind_Set(bind, key); + } +} + +static int KeyBindsScreen_KeyDown(void* screen, int key) { + struct KeyBindsScreen* s = (struct KeyBindsScreen*)screen; + InputBind bind; + int idx; + + if (s->curI == -1) return Menu_InputDown(s, key); + bind = s->binds[s->curI]; + + if (Input_IsEscapeButton(key)) { + KeyBindsScreen_ResetBinding(bind); + } else { + KeyBindsScreen_UpdateBinding(bind, key); + } + + idx = s->curI; + s->curI = -1; + s->closable = true; + KeyBindsScreen_Update(s, idx); + return true; +} + +static void KeyBindsScreen_ContextLost(void* screen) { + struct KeyBindsScreen* s = (struct KeyBindsScreen*)screen; + Font_Free(&s->titleFont); + Screen_ContextLost(screen); +} + +static void KeyBindsScreen_ContextRecreated(void* screen) { + struct KeyBindsScreen* s = (struct KeyBindsScreen*)screen; + struct FontDesc textFont; + int i; + + Screen_UpdateVb(screen); + Gui_MakeTitleFont(&s->titleFont); + Gui_MakeBodyFont(&textFont); + for (i = 0; i < s->bindsCount; i++) { KeyBindsScreen_Update(s, i); } + + TextWidget_SetConst(&s->title, s->titleText, &s->titleFont); + TextWidget_SetConst(&s->msg, s->msgText, &textFont); + ButtonWidget_SetConst(&s->back, "Done", &s->titleFont); + + Font_Free(&textFont); + if (!s->leftPage && !s->rightPage) return; + ButtonWidget_SetConst(&s->left, "<", &s->titleFont); + ButtonWidget_SetConst(&s->right, ">", &s->titleFont); +} + +static void KeyBindsScreen_Layout(void* screen) { + struct KeyBindsScreen* s = (struct KeyBindsScreen*)screen; + int i, x, y, xDir, leftLen; + x = s->btnWidth / 2 + 5; + y = s->topY; + + leftLen = s->leftLen; + for (i = 0; i < s->bindsCount; i++) { + if (i == leftLen) y = s->topY; /* reset y for next column */ + xDir = leftLen == -1 ? 0 : (i < leftLen ? -1 : 1); + + Widget_SetLocation(&s->buttons[i], ANCHOR_CENTRE, ANCHOR_CENTRE, x * xDir, y); + y += 50; /* distance between buttons */ + } + + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -180); + Widget_SetLocation(&s->msg, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 100); + Menu_LayoutBack(&s->back); + + Widget_SetLocation(&s->left, ANCHOR_CENTRE, ANCHOR_CENTRE, -s->btnWidth - 35, s->arrowsY); + Widget_SetLocation(&s->right, ANCHOR_CENTRE, ANCHOR_CENTRE, s->btnWidth + 35, s->arrowsY); +} + +static void KeyBindsScreen_Init(void* screen) { + struct KeyBindsScreen* s = (struct KeyBindsScreen*)screen; + int i; + s->widgets = key_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(key_widgets); + s->curI = -1; + + for (i = 0; i < s->bindsCount; i++) + { + ButtonWidget_Add(s, &s->buttons[i], s->btnWidth, KeyBindsScreen_OnBindingClick); + s->widgets[i] = (struct Widget*)&s->buttons[i]; + s->buttons[i].meta.val = i; + } + + TextWidget_Add(s, &s->title); + TextWidget_Add(s, &s->msg); + ButtonWidget_Add(s, &s->back, 400, Gui.ClassicMenu ? Menu_SwitchClassicOptions : Menu_SwitchOptions); + + if (s->leftPage || s->rightPage) { + ButtonWidget_Add(s, &s->left, 40, s->leftPage); + ButtonWidget_Add(s, &s->right, 40, s->rightPage); + Widget_SetDisabled(&s->left, !s->leftPage); + Widget_SetDisabled(&s->right, !s->rightPage); + } + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE KeyBindsScreen_VTABLE = { + KeyBindsScreen_Init, Screen_NullUpdate, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + KeyBindsScreen_KeyDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + KeyBindsScreen_Layout, KeyBindsScreen_ContextLost, KeyBindsScreen_ContextRecreated +}; + +static void KeyBindsScreen_Reset(Widget_LeftClick left, Widget_LeftClick right, int btnWidth) { + struct KeyBindsScreen* s = &KeyBindsScreen; + s->leftPage = left; + s->rightPage = right; + s->btnWidth = btnWidth; + s->msgText = ""; +} +static void KeyBindsScreen_SetLayout(int topY, int arrowsY, int leftLen) { + struct KeyBindsScreen* s = &KeyBindsScreen; + s->topY = topY; + s->arrowsY = arrowsY; + s->leftLen = leftLen; +} +static void KeyBindsScreen_Show(int bindsCount, const cc_uint8* binds, const char* const* descs, const char* title) { + struct KeyBindsScreen* s = &KeyBindsScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &KeyBindsScreen_VTABLE; + + s->titleText = title; + s->bindsCount = bindsCount; + s->binds = binds; + s->descs = descs; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*------------------------------------------------ClassicBindingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +void ClassicBindingsScreen_Show(void) { + static const cc_uint8 binds[] = { BIND_FORWARD, BIND_BACK, BIND_JUMP, BIND_CHAT, BIND_SET_SPAWN, BIND_LEFT, BIND_RIGHT, BIND_INVENTORY, BIND_FOG, BIND_RESPAWN }; + static const char* const descs[] = { "Forward", "Back", "Jump", "Chat", "Save location", "Left", "Right", "Build", "Toggle fog", "Load location" }; + binds_gamepad = false; + + if (Game_ClassicHacks) { + KeyBindsScreen_Reset(NULL, Menu_SwitchBindsClassicHacks, 260); + } else { + KeyBindsScreen_Reset(NULL, NULL, 300); + } + KeyBindsScreen_SetLayout(-140, -40, 5); + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, + Game_ClassicHacks ? "Normal controls" : "Controls"); +} + + +/*########################################################################################################################* +*----------------------------------------------ClassicHacksBindingsScreen-------------------------------------------------* +*#########################################################################################################################*/ +void ClassicHacksBindingsScreen_Show(void) { + static const cc_uint8 binds[6] = { BIND_SPEED, BIND_NOCLIP, BIND_HALF_SPEED, BIND_FLY, BIND_FLY_UP, BIND_FLY_DOWN }; + static const char* const descs[6] = { "Speed", "Noclip", "Half speed", "Fly", "Fly up", "Fly down" }; + binds_gamepad = false; + + KeyBindsScreen_Reset(Menu_SwitchBindsClassic, NULL, 260); + KeyBindsScreen_SetLayout(-90, -40, 3); + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, "Hacks controls"); +} + + +/*########################################################################################################################* +*-------------------------------------------------NormalBindingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +void NormalBindingsScreen_Show(void) { + static const cc_uint8 binds[] = { BIND_FORWARD, BIND_BACK, BIND_JUMP, BIND_CHAT, BIND_SET_SPAWN, BIND_TABLIST, BIND_LEFT, BIND_RIGHT, BIND_INVENTORY, BIND_FOG, BIND_RESPAWN, BIND_SEND_CHAT }; + static const char* const descs[] = { "Forward", "Back", "Jump", "Chat", "Set spawn", "Player list", "Left", "Right", "Inventory", "Toggle fog", "Respawn", "Send chat" }; + + KeyBindsScreen_Reset(NULL, Menu_SwitchBindsHacks, 250); + KeyBindsScreen_SetLayout(-140, 10, 6); + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, "Normal controls"); +} + + +/*########################################################################################################################* +*--------------------------------------------------HacksBindingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +void HacksBindingsScreen_Show(void) { + static const cc_uint8 binds[] = { BIND_SPEED, BIND_NOCLIP, BIND_HALF_SPEED, BIND_ZOOM_SCROLL, BIND_FLY, BIND_FLY_UP, BIND_FLY_DOWN, BIND_THIRD_PERSON }; + static const char* const descs[] = { "Speed", "Noclip", "Half speed", "Scroll zoom", "Fly", "Fly up", "Fly down", "Third person" }; + + KeyBindsScreen_Reset(Menu_SwitchBindsNormal, Menu_SwitchBindsOther, 260); + KeyBindsScreen_SetLayout(-40, 10, 4); + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, "Hacks controls"); +} + + +/*########################################################################################################################* +*--------------------------------------------------OtherBindingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +void OtherBindingsScreen_Show(void) { + static const cc_uint8 binds[] = { BIND_EXT_INPUT, BIND_HIDE_FPS, BIND_HIDE_GUI, BIND_HOTBAR_SWITCH, BIND_DROP_BLOCK,BIND_SCREENSHOT, BIND_FULLSCREEN, BIND_AXIS_LINES, BIND_AUTOROTATE, BIND_SMOOTH_CAMERA, BIND_IDOVERLAY, BIND_BREAK_LIQUIDS }; + static const char* const descs[] = { "Show ext input", "Hide FPS", "Hide gui", "Hotbar switching", "Drop block", "Screenshot", "Fullscreen", "Show axis lines", "Auto-rotate", "Smooth camera", "ID overlay", "Breakable liquids" }; + + KeyBindsScreen_Reset(Menu_SwitchBindsHacks, Menu_SwitchBindsMouse, 260); + KeyBindsScreen_SetLayout(-140, 10, 6); + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, "Other controls"); +} + + +/*########################################################################################################################* +*--------------------------------------------------MouseBindingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +void MouseBindingsScreen_Show(void) { + static const cc_uint8 binds[] = { BIND_DELETE_BLOCK, BIND_PICK_BLOCK, BIND_PLACE_BLOCK, BIND_LOOK_UP, BIND_LOOK_DOWN, BIND_LOOK_LEFT, BIND_LOOK_RIGHT }; + static const char* const descs[] = { "Delete block", "Pick block", "Place block", "Look Up", "Look Down", "Look Left", "Look Right" }; + + KeyBindsScreen_Reset(Menu_SwitchBindsOther, Menu_SwitchBindsHotbar, 260); + KeyBindsScreen_SetLayout(-140, 10, 3); + KeyBindsScreen.msgText = "&ePress escape to reset the binding"; + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, "Mouse key bindings"); +} + + +/*########################################################################################################################* +*-------------------------------------------------HotbarBindingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +void HotbarBindingsScreen_Show(void) { + static const cc_uint8 binds[] = { BIND_HOTBAR_1,BIND_HOTBAR_2,BIND_HOTBAR_3, BIND_HOTBAR_4,BIND_HOTBAR_5,BIND_HOTBAR_6, BIND_HOTBAR_7,BIND_HOTBAR_8,BIND_HOTBAR_9, + BIND_HOTBAR_LEFT, BIND_HOTBAR_RIGHT }; + static const char* const descs[] = { "Slot #1","Slot #2","Slot #3", "Slot #4","Slot #5","Slot #6", "Slot #7","Slot #8","Slot #9", "Slot left","Slot right" }; + + KeyBindsScreen_Reset(Menu_SwitchBindsMouse, NULL, 260); + KeyBindsScreen_SetLayout(-140, 10, 6); + KeyBindsScreen_Show(Array_Elems(binds), binds, descs, "Hotbar controls"); +} + + + +/*########################################################################################################################* +*--------------------------------------------------MenuInputOverlay-------------------------------------------------------* +*#########################################################################################################################*/ +static struct MenuInputOverlay { + Screen_Body + cc_bool screenMode; + struct FontDesc textFont; + struct ButtonWidget ok, Default; + struct TextInputWidget input; + struct MenuInputDesc* desc; + MenuInputDone onDone; + cc_string value; char valueBuffer[STRING_SIZE]; +} MenuInputOverlay; + +static struct Widget* menuInput_widgets[2 + 1]; + +static void MenuInputOverlay_Close(struct MenuInputOverlay* s, cc_bool valid) { + Gui_Remove((struct Screen*)&MenuInputOverlay); + s->onDone(&s->input.base.text, valid); +} + +static void MenuInputOverlay_EnterInput(struct MenuInputOverlay* s) { + cc_bool valid = s->desc->VTABLE->IsValidValue(s->desc, &s->input.base.text); + MenuInputOverlay_Close(s, valid); +} + +static int MenuInputOverlay_KeyPress(void* screen, char keyChar) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + InputWidget_Append(&s->input.base, keyChar); + return true; +} + +static int MenuInputOverlay_TextChanged(void* screen, const cc_string* str) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + InputWidget_SetText(&s->input.base, str); + return true; +} + +static int MenuInputOverlay_KeyDown(void* screen, int key) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + if (Elem_HandlesKeyDown(&s->input.base, key)) return true; + + if (Input_IsEnterButton(key)) { + MenuInputOverlay_EnterInput(s); return true; + } + return Menu_InputDown(screen, key); +} + +static int MenuInputOverlay_PointerDown(void* screen, int id, int x, int y) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + return Screen_DoPointerDown(screen, id, x, y) >= 0 || s->screenMode; +} + +static int MenuInputOverlay_PointerMove(void* screen, int id, int x, int y) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + return Menu_DoPointerMove(screen, id, x, y) >= 0 || s->screenMode; +} + +static void MenuInputOverlay_OK(void* screen, void* widget) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + MenuInputOverlay_EnterInput(s); +} + +static void MenuInputOverlay_Default(void* screen, void* widget) { + cc_string value; char valueBuffer[STRING_SIZE]; + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + + String_InitArray(value, valueBuffer); + s->desc->VTABLE->GetDefault(s->desc, &value); + InputWidget_SetText(&s->input.base, &value); +} + +static void MenuInputOverlay_Init(void* screen) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + s->widgets = menuInput_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(menuInput_widgets); + + ButtonWidget_Add(s, &s->ok, Input_TouchMode ? 200 : 40, MenuInputOverlay_OK); + ButtonWidget_Add(s, &s->Default, 200, MenuInputOverlay_Default); + TextInputWidget_Add(s, &s->input, 400, &s->value, s->desc); + + if (s->desc->VTABLE == &IntInput_VTABLE) { + s->input.onscreenType = KEYBOARD_TYPE_INTEGER; + } else if (s->desc->VTABLE == &FloatInput_VTABLE) { + s->input.onscreenType = KEYBOARD_TYPE_NUMBER; + } + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static void MenuInputOverlay_Update(void* screen, float delta) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + s->input.base.caretAccumulator += delta; +} + +static void MenuInputOverlay_Render(void* screen, float delta) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + if (s->screenMode) Menu_RenderBounds(); + + Screen_Render2Widgets(screen, delta); +} + +static void MenuInputOverlay_Free(void* screen) { + OnscreenKeyboard_Close(); +} + +static void MenuInputOverlay_Layout(void* screen) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + if (!Input_TouchMode) { + Widget_SetLocation(&s->input, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 110); + Widget_SetLocation(&s->ok, ANCHOR_CENTRE, ANCHOR_CENTRE, 240, 110); + Widget_SetLocation(&s->Default, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 150); + } else if (Window_Main.SoftKeyboard == SOFT_KEYBOARD_SHIFT) { + Widget_SetLocation(&s->input, ANCHOR_CENTRE, ANCHOR_MAX, 0, 65); + Widget_SetLocation(&s->ok, ANCHOR_CENTRE, ANCHOR_MAX, 120, 25); + Widget_SetLocation(&s->Default, ANCHOR_CENTRE, ANCHOR_MAX, -120, 25); + } else { + Widget_SetLocation(&s->input, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 110); + Widget_SetLocation(&s->ok, ANCHOR_CENTRE, ANCHOR_CENTRE, 120, 150); + Widget_SetLocation(&s->Default, ANCHOR_CENTRE, ANCHOR_CENTRE, -120, 150); + } +} + +static void MenuInputOverlay_ContextLost(void* screen) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + Font_Free(&s->textFont); + Screen_ContextLost(s); +} + +static void MenuInputOverlay_ContextRecreated(void* screen) { + struct MenuInputOverlay* s = (struct MenuInputOverlay*)screen; + struct FontDesc font; + Gui_MakeTitleFont(&font); + Gui_MakeBodyFont(&s->textFont); + Screen_UpdateVb(s); + + TextInputWidget_SetFont(&s->input, &s->textFont); + ButtonWidget_SetConst(&s->ok, "OK", &font); + ButtonWidget_SetConst(&s->Default, "Default value", &font); + Font_Free(&font); +} + +static const struct ScreenVTABLE MenuInputOverlay_VTABLE = { + MenuInputOverlay_Init, MenuInputOverlay_Update, MenuInputOverlay_Free, + MenuInputOverlay_Render, Screen_BuildMesh, + MenuInputOverlay_KeyDown, Screen_InputUp, MenuInputOverlay_KeyPress, MenuInputOverlay_TextChanged, + MenuInputOverlay_PointerDown, Screen_PointerUp, MenuInputOverlay_PointerMove, Screen_TMouseScroll, + MenuInputOverlay_Layout, MenuInputOverlay_ContextLost, MenuInputOverlay_ContextRecreated +}; +void MenuInputOverlay_Show(struct MenuInputDesc* desc, const cc_string* value, MenuInputDone onDone, cc_bool screenMode) { + struct MenuInputOverlay* s = &MenuInputOverlay; + s->grabsInput = true; + s->closable = true; + s->desc = desc; + s->onDone = onDone; + s->screenMode = screenMode; + s->VTABLE = &MenuInputOverlay_VTABLE; + + String_InitArray(s->value, s->valueBuffer); + String_Copy(&s->value, value); + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENUINPUT); +} + + +/*########################################################################################################################* +*--------------------------------------------------MenuOptionsScreen------------------------------------------------------* +*#########################################################################################################################*/ +struct MenuOptionsScreen; +typedef void (*InitMenuOptions)(struct MenuOptionsScreen* s); +#define MENUOPTS_MAX_OPTS 11 +static void MenuOptionsScreen_Layout(void* screen); + +static struct MenuOptionsScreen { + Screen_Body + const char* descriptions[MENUOPTS_MAX_OPTS + 1]; + struct ButtonWidget* activeBtn; + InitMenuOptions DoInit, DoRecreateExtra, OnHacksChanged, OnLightingModeServerChanged; + int numButtons; + struct FontDesc titleFont, textFont; + struct TextGroupWidget extHelp; + struct Texture extHelpTextures[5]; /* max lines is 5 */ + struct ButtonWidget buttons[MENUOPTS_MAX_OPTS], done; + const char* extHelpDesc; +} MenuOptionsScreen_Instance; + +static struct MenuInputDesc menuOpts_descs[MENUOPTS_MAX_OPTS]; +static struct Widget* menuOpts_widgets[MENUOPTS_MAX_OPTS + 1]; + +static void Menu_GetBool(cc_string* raw, cc_bool v) { + String_AppendConst(raw, v ? "ON" : "OFF"); +} +static cc_bool Menu_SetBool(const cc_string* raw, const char* key) { + cc_bool isOn = String_CaselessEqualsConst(raw, "ON"); + Options_SetBool(key, isOn); + return isOn; +} + +static void MenuOptionsScreen_GetFPS(cc_string* raw) { + String_AppendConst(raw, FpsLimit_Names[Game_FpsLimit]); +} +static void MenuOptionsScreen_SetFPS(const cc_string* v) { + int method = Utils_ParseEnum(v, FPS_LIMIT_VSYNC, FpsLimit_Names, Array_Elems(FpsLimit_Names)); + Options_Set(OPT_FPS_LIMIT, v); + Game_SetFpsLimit(method); +} + +static void MenuOptionsScreen_Update(struct MenuOptionsScreen* s, struct ButtonWidget* btn) { + cc_string title; char titleBuffer[STRING_SIZE]; + String_InitArray(title, titleBuffer); + + String_AppendConst(&title, btn->optName); + if (btn->GetValue) { + String_AppendConst(&title, ": "); + btn->GetValue(&title); + } + ButtonWidget_Set(btn, &title, &s->titleFont); +} + +CC_NOINLINE static void MenuOptionsScreen_Set(struct MenuOptionsScreen* s, + struct ButtonWidget* btn, const cc_string* text) { + btn->SetValue(text); + MenuOptionsScreen_Update(s, btn); +} + +CC_NOINLINE static void MenuOptionsScreen_FreeExtHelp(struct MenuOptionsScreen* s) { + Elem_Free(&s->extHelp); + s->extHelp.lines = 0; +} + +static void MenuOptionsScreen_LayoutExtHelp(struct MenuOptionsScreen* s) { + Widget_SetLocation(&s->extHelp, ANCHOR_MIN, ANCHOR_CENTRE_MIN, 0, 100); + /* If use centre align above, then each line in extended help gets */ + /* centered aligned separately - which is not the desired behaviour. */ + s->extHelp.xOffset = Window_UI.Width / 2 - s->extHelp.width / 2; + Widget_Layout(&s->extHelp); +} + +static cc_string MenuOptionsScreen_GetDesc(int i) { + const char* desc = MenuOptionsScreen_Instance.extHelpDesc; + cc_string descRaw, descLines[5]; + + descRaw = String_FromReadonly(desc); + String_UNSAFE_Split(&descRaw, '\n', descLines, Array_Elems(descLines)); + return descLines[i]; +} + +static void MenuOptionsScreen_SelectExtHelp(struct MenuOptionsScreen* s, int idx) { + const char* desc; + cc_string descRaw, descLines[5]; + + MenuOptionsScreen_FreeExtHelp(s); + if (s->activeBtn) return; + desc = s->descriptions[idx]; + if (!desc) return; + + if (!s->widgets[idx]) return; + if (s->widgets[idx]->flags & WIDGET_FLAG_DISABLED) return; + + descRaw = String_FromReadonly(desc); + s->extHelp.lines = String_UNSAFE_Split(&descRaw, '\n', descLines, Array_Elems(descLines)); + + s->extHelpDesc = desc; + TextGroupWidget_RedrawAll(&s->extHelp); + MenuOptionsScreen_LayoutExtHelp(s); +} + +static void MenuOptionsScreen_OnDone(const cc_string* value, cc_bool valid) { + struct MenuOptionsScreen* s = &MenuOptionsScreen_Instance; + if (valid) { + MenuOptionsScreen_Set(s, s->activeBtn, value); + /* Marking screen as dirty fixes changed option widget appearing wrong */ + /* for a few frames (e.g. Chatlines options changed from '12' to '1') */ + s->dirty = true; + } + + if (s->selectedI >= 0) MenuOptionsScreen_SelectExtHelp(s, s->selectedI); + s->activeBtn = NULL; +} + +static int MenuOptionsScreen_PointerMove(void* screen, int id, int x, int y) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + int i = Menu_DoPointerMove(s, id, x, y); + if (i == -1 || i == s->selectedI) return true; + + s->selectedI = i; + if (!s->activeBtn) MenuOptionsScreen_SelectExtHelp(s, i); + return true; +} + +static void MenuOptionsScreen_AddButtons(struct MenuOptionsScreen* s, const struct MenuOptionDesc* btns, int count, Widget_LeftClick backClick) { + struct ButtonWidget* btn; + int i; + + for (i = 0; i < count; i++) { + btn = &s->buttons[i]; + ButtonWidget_Add(s, btn, 300, btns[i].OnClick); + Widget_SetLocation(btn, ANCHOR_CENTRE, ANCHOR_CENTRE, btns[i].dir * 160, btns[i].y); + + btn->optName = btns[i].name; + btn->GetValue = btns[i].GetValue; + btn->SetValue = btns[i].SetValue; + btn->meta.ptr = &menuOpts_descs[i]; + s->widgets[i] = (struct Widget*)btn; + } + s->numButtons = count; + ButtonWidget_Add(s, &s->done, 400, backClick); +} + +static void MenuOptionsScreen_Bool(void* screen, void* widget) { + cc_string value; char valueBuffer[STRING_SIZE]; + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + struct ButtonWidget* btn = (struct ButtonWidget*)widget; + cc_bool isOn; + + String_InitArray(value, valueBuffer); + btn->GetValue(&value); + + isOn = String_CaselessEqualsConst(&value, "ON"); + value = String_FromReadonly(isOn ? "OFF" : "ON"); + MenuOptionsScreen_Set(s, btn, &value); +} + +static void MenuOptionsScreen_Enum(void* screen, void* widget) { + cc_string value; char valueBuffer[STRING_SIZE]; + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + struct ButtonWidget* btn = (struct ButtonWidget*)widget; + struct MenuInputDesc* desc; + const char* const* names; + int raw, count; + + String_InitArray(value, valueBuffer); + btn->GetValue(&value); + + desc = (struct MenuInputDesc*)btn->meta.ptr; + names = desc->meta.e.Names; + count = desc->meta.e.Count; + + raw = (Utils_ParseEnum(&value, 0, names, count) + 1) % count; + value = String_FromReadonly(names[raw]); + MenuOptionsScreen_Set(s, btn, &value); +} + +static void MenuInputOverlay_CheckStillValid(struct MenuOptionsScreen* s) { + if (!s->activeBtn) return; + + if (s->activeBtn->flags & WIDGET_FLAG_DISABLED) { + /* source button is disabled now, so close open input overlay */ + MenuInputOverlay_Close(&MenuInputOverlay, false); + } +} + +static void MenuOptionsScreen_Input(void* screen, void* widget) { + cc_string value; char valueBuffer[STRING_SIZE]; + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + struct ButtonWidget* btn = (struct ButtonWidget*)widget; + struct MenuInputDesc* desc; + + MenuOptionsScreen_FreeExtHelp(s); + s->activeBtn = btn; + + String_InitArray(value, valueBuffer); + btn->GetValue(&value); + desc = (struct MenuInputDesc*)btn->meta.ptr; + MenuInputOverlay_Show(desc, &value, MenuOptionsScreen_OnDone, Gui_TouchUI); +} + +static void MenuOptionsScreen_OnHacksChanged(void* screen) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + if (s->OnHacksChanged) s->OnHacksChanged(s); + s->dirty = true; +} +static void MenuOptionsScreen_OnLightingModeServerChanged(void* screen, cc_uint8 oldMode, cc_bool fromServer) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + /* This event only actually matters if it's from the server */ + if (fromServer) { + if (s->OnLightingModeServerChanged) s->OnLightingModeServerChanged(s); + s->dirty = true; + } +} + +static void MenuOptionsScreen_Init(void* screen) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + int i; + + s->widgets = menuOpts_widgets; + s->numWidgets = 0; + s->maxWidgets = MENUOPTS_MAX_OPTS + 1; /* always have back button */ + + /* The various menu options screens might have different number of widgets */ + for (i = 0; i < MENUOPTS_MAX_OPTS; i++) { + s->widgets[i] = NULL; + s->descriptions[i] = NULL; + } + + s->activeBtn = NULL; + s->selectedI = -1; + s->DoInit(s); + + TextGroupWidget_Create(&s->extHelp, 5, s->extHelpTextures, MenuOptionsScreen_GetDesc); + s->extHelp.lines = 0; + Event_Register_(&UserEvents.HackPermsChanged, screen, MenuOptionsScreen_OnHacksChanged); + Event_Register_(&WorldEvents.LightingModeChanged, screen, MenuOptionsScreen_OnLightingModeServerChanged); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +#define EXTHELP_PAD 5 /* padding around extended help box */ +static void MenuOptionsScreen_Render(void* screen, float delta) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + struct TextGroupWidget* w; + PackedCol tableColor = PackedCol_Make(20, 20, 20, 200); + + MenuScreen_Render2(s, delta); + if (!s->extHelp.lines) return; + + w = &s->extHelp; + Gfx_Draw2DFlat(w->x - EXTHELP_PAD, w->y - EXTHELP_PAD, + w->width + EXTHELP_PAD * 2, w->height + EXTHELP_PAD * 2, tableColor); + + Elem_Render(&s->extHelp, delta); +} + +static void MenuOptionsScreen_Free(void* screen) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + Event_Unregister_(&UserEvents.HackPermsChanged, screen, MenuOptionsScreen_OnHacksChanged); + Event_Unregister_(&WorldEvents.LightingModeChanged, screen, MenuOptionsScreen_OnLightingModeServerChanged); + Gui_RemoveCore((struct Screen*)&MenuInputOverlay); +} + +static void MenuOptionsScreen_Layout(void* screen) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + Screen_Layout(s); + Menu_LayoutBack(&s->done); + MenuOptionsScreen_LayoutExtHelp(s); +} + +static void MenuOptionsScreen_ContextLost(void* screen) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + Font_Free(&s->titleFont); + Font_Free(&s->textFont); + Screen_ContextLost(s); + Elem_Free(&s->extHelp); +} + +static void MenuOptionsScreen_ContextRecreated(void* screen) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + int i; + Gui_MakeTitleFont(&s->titleFont); + Gui_MakeBodyFont(&s->textFont); + Screen_UpdateVb(screen); + + for (i = 0; i < s->numButtons; i++) + { + if (s->widgets[i]) MenuOptionsScreen_Update(s, &s->buttons[i]); + } + + ButtonWidget_SetConst(&s->done, "Done", &s->titleFont); + if (s->DoRecreateExtra) s->DoRecreateExtra(s); + TextGroupWidget_SetFont(&s->extHelp, &s->textFont); + TextGroupWidget_RedrawAll(&s->extHelp); /* TODO: SetFont should redrawall implicitly */ +} + +static const struct ScreenVTABLE MenuOptionsScreen_VTABLE = { + MenuOptionsScreen_Init, Screen_NullUpdate, MenuOptionsScreen_Free, + MenuOptionsScreen_Render, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, MenuOptionsScreen_PointerMove, Screen_TMouseScroll, + MenuOptionsScreen_Layout, MenuOptionsScreen_ContextLost, MenuOptionsScreen_ContextRecreated +}; +void MenuOptionsScreen_Show(InitMenuOptions init) { + struct MenuOptionsScreen* s = &MenuOptionsScreen_Instance; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &MenuOptionsScreen_VTABLE; + + s->DoInit = init; + s->DoRecreateExtra = NULL; + s->OnHacksChanged = NULL; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*---------------------------------------------------ClassicOptionsScreen--------------------------------------------------* +*#########################################################################################################################*/ +enum ViewDist { VIEW_TINY, VIEW_SHORT, VIEW_NORMAL, VIEW_FAR, VIEW_COUNT }; +static const char* const viewDistNames[VIEW_COUNT] = { "TINY", "SHORT", "NORMAL", "FAR" }; + +static void ClassicOptionsScreen_GetMusic(cc_string* v) { Menu_GetBool(v, Audio_MusicVolume > 0); } +static void ClassicOptionsScreen_SetMusic(const cc_string* v) { + Audio_SetMusic(String_CaselessEqualsConst(v, "ON") ? 100 : 0); + Options_SetInt(OPT_MUSIC_VOLUME, Audio_MusicVolume); +} + +static void ClassicOptionsScreen_GetInvert(cc_string* v) { Menu_GetBool(v, Camera.Invert); } +static void ClassicOptionsScreen_SetInvert(const cc_string* v) { Camera.Invert = Menu_SetBool(v, OPT_INVERT_MOUSE); } + +static void ClassicOptionsScreen_GetViewDist(cc_string* v) { + if (Game_ViewDistance >= 512) { + String_AppendConst(v, viewDistNames[VIEW_FAR]); + } else if (Game_ViewDistance >= 128) { + String_AppendConst(v, viewDistNames[VIEW_NORMAL]); + } else if (Game_ViewDistance >= 32) { + String_AppendConst(v, viewDistNames[VIEW_SHORT]); + } else { + String_AppendConst(v, viewDistNames[VIEW_TINY]); + } +} +static void ClassicOptionsScreen_SetViewDist(const cc_string* v) { + int raw = Utils_ParseEnum(v, 0, viewDistNames, VIEW_COUNT); + int dist = raw == VIEW_FAR ? 512 : (raw == VIEW_NORMAL ? 128 : (raw == VIEW_SHORT ? 32 : 8)); + Game_UserSetViewDistance(dist); +} + +static void ClassicOptionsScreen_GetAnaglyph(cc_string* v) { Menu_GetBool(v, Game_Anaglyph3D); } +static void ClassicOptionsScreen_SetAnaglyph(const cc_string* v) { + Game_Anaglyph3D = Menu_SetBool(v, OPT_ANAGLYPH3D); +} + +static void ClassicOptionsScreen_GetSounds(cc_string* v) { Menu_GetBool(v, Audio_SoundsVolume > 0); } +static void ClassicOptionsScreen_SetSounds(const cc_string* v) { + Audio_SetSounds(String_CaselessEqualsConst(v, "ON") ? 100 : 0); + Options_SetInt(OPT_SOUND_VOLUME, Audio_SoundsVolume); +} + +static void ClassicOptionsScreen_GetShowFPS(cc_string* v) { Menu_GetBool(v, Gui.ShowFPS); } +static void ClassicOptionsScreen_SetShowFPS(const cc_string* v) { Gui.ShowFPS = Menu_SetBool(v, OPT_SHOW_FPS); } + +static void ClassicOptionsScreen_GetViewBob(cc_string* v) { Menu_GetBool(v, Game_ViewBobbing); } +static void ClassicOptionsScreen_SetViewBob(const cc_string* v) { Game_ViewBobbing = Menu_SetBool(v, OPT_VIEW_BOBBING); } + +static void ClassicOptionsScreen_GetHacks(cc_string* v) { Menu_GetBool(v, Entities.CurPlayer->Hacks.Enabled); } +static void ClassicOptionsScreen_SetHacks(const cc_string* v) { + Entities.CurPlayer->Hacks.Enabled = Menu_SetBool(v, OPT_HACKS_ENABLED); + HacksComp_Update(&Entities.CurPlayer->Hacks); +} + +static void ClassicOptionsScreen_RecreateExtra(struct MenuOptionsScreen* s) { + ButtonWidget_SetConst(&s->buttons[9], "Controls...", &s->titleFont); +} + +static void ClassicOptionsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -150, "Music", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetMusic, ClassicOptionsScreen_SetMusic }, + { -1, -100, "Invert mouse", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetInvert, ClassicOptionsScreen_SetInvert }, + { -1, -50, "Render distance", MenuOptionsScreen_Enum, + ClassicOptionsScreen_GetViewDist, ClassicOptionsScreen_SetViewDist }, + { -1, 0, "3D anaglyph", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetAnaglyph, ClassicOptionsScreen_SetAnaglyph }, + + { 1, -150, "Sound", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetSounds, ClassicOptionsScreen_SetSounds }, + { 1, -100, "Show FPS", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetShowFPS, ClassicOptionsScreen_SetShowFPS }, + { 1, -50, "View bobbing", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetViewBob, ClassicOptionsScreen_SetViewBob }, + { 1, 0, "FPS mode", MenuOptionsScreen_Enum, + MenuOptionsScreen_GetFPS, MenuOptionsScreen_SetFPS }, + { 0, 60, "Hacks enabled", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetHacks, ClassicOptionsScreen_SetHacks } + }; + s->DoRecreateExtra = ClassicOptionsScreen_RecreateExtra; + + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchPause); + ButtonWidget_Add(s, &s->buttons[9], 400, Menu_SwitchBindsClassic); + Widget_SetLocation(&s->buttons[9], ANCHOR_CENTRE, ANCHOR_MAX, 0, 95); + + /* Disable certain options */ + if (!Server.IsSinglePlayer) Menu_Remove(s, 3); + if (!Game_ClassicHacks) Menu_Remove(s, 8); +} + +void ClassicOptionsScreen_Show(void) { + MenuInput_Enum(menuOpts_descs[2], viewDistNames, VIEW_COUNT); + MenuInput_Enum(menuOpts_descs[7], FpsLimit_Names, FPS_LIMIT_COUNT); + + MenuOptionsScreen_Show(ClassicOptionsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*----------------------------------------------------EnvSettingsScreen----------------------------------------------------* +*#########################################################################################################################*/ +static void EnvSettingsScreen_GetCloudsColor(cc_string* v) { PackedCol_ToHex(v, Env.CloudsCol); } +static void EnvSettingsScreen_SetCloudsColor(const cc_string* v) { Env_SetCloudsCol(Menu_HexCol(v)); } + +static void EnvSettingsScreen_GetSkyColor(cc_string* v) { PackedCol_ToHex(v, Env.SkyCol); } +static void EnvSettingsScreen_SetSkyColor(const cc_string* v) { Env_SetSkyCol(Menu_HexCol(v)); } + +static void EnvSettingsScreen_GetFogColor(cc_string* v) { PackedCol_ToHex(v, Env.FogCol); } +static void EnvSettingsScreen_SetFogColor(const cc_string* v) { Env_SetFogCol(Menu_HexCol(v)); } + +static void EnvSettingsScreen_GetCloudsSpeed(cc_string* v) { String_AppendFloat(v, Env.CloudsSpeed, 2); } +static void EnvSettingsScreen_SetCloudsSpeed(const cc_string* v) { Env_SetCloudsSpeed(Menu_Float(v)); } + +static void EnvSettingsScreen_GetCloudsHeight(cc_string* v) { String_AppendInt(v, Env.CloudsHeight); } +static void EnvSettingsScreen_SetCloudsHeight(const cc_string* v) { Env_SetCloudsHeight(Menu_Int(v)); } + +static void EnvSettingsScreen_GetSunColor(cc_string* v) { PackedCol_ToHex(v, Env.SunCol); } +static void EnvSettingsScreen_SetSunColor(const cc_string* v) { Env_SetSunCol(Menu_HexCol(v)); } + +static void EnvSettingsScreen_GetShadowColor(cc_string* v) { PackedCol_ToHex(v, Env.ShadowCol); } +static void EnvSettingsScreen_SetShadowColor(const cc_string* v) { Env_SetShadowCol(Menu_HexCol(v)); } + +static void EnvSettingsScreen_GetWeather(cc_string* v) { String_AppendConst(v, Weather_Names[Env.Weather]); } +static void EnvSettingsScreen_SetWeather(const cc_string* v) { + int raw = Utils_ParseEnum(v, 0, Weather_Names, Array_Elems(Weather_Names)); + Env_SetWeather(raw); +} + +static void EnvSettingsScreen_GetWeatherSpeed(cc_string* v) { String_AppendFloat(v, Env.WeatherSpeed, 2); } +static void EnvSettingsScreen_SetWeatherSpeed(const cc_string* v) { Env_SetWeatherSpeed(Menu_Float(v)); } + +static void EnvSettingsScreen_GetEdgeHeight(cc_string* v) { String_AppendInt(v, Env.EdgeHeight); } +static void EnvSettingsScreen_SetEdgeHeight(const cc_string* v) { Env_SetEdgeHeight(Menu_Int(v)); } + +static void EnvSettingsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -150, "Clouds color", MenuOptionsScreen_Input, + EnvSettingsScreen_GetCloudsColor, EnvSettingsScreen_SetCloudsColor }, + { -1, -100, "Sky color", MenuOptionsScreen_Input, + EnvSettingsScreen_GetSkyColor, EnvSettingsScreen_SetSkyColor }, + { -1, -50, "Fog color", MenuOptionsScreen_Input, + EnvSettingsScreen_GetFogColor, EnvSettingsScreen_SetFogColor }, + { -1, 0, "Clouds speed", MenuOptionsScreen_Input, + EnvSettingsScreen_GetCloudsSpeed, EnvSettingsScreen_SetCloudsSpeed }, + { -1, 50, "Clouds height", MenuOptionsScreen_Input, + EnvSettingsScreen_GetCloudsHeight, EnvSettingsScreen_SetCloudsHeight }, + + { 1, -150, "Sunlight color", MenuOptionsScreen_Input, + EnvSettingsScreen_GetSunColor, EnvSettingsScreen_SetSunColor }, + { 1, -100, "Shadow color", MenuOptionsScreen_Input, + EnvSettingsScreen_GetShadowColor, EnvSettingsScreen_SetShadowColor }, + { 1, -50, "Weather", MenuOptionsScreen_Enum, + EnvSettingsScreen_GetWeather, EnvSettingsScreen_SetWeather }, + { 1, 0, "Rain/Snow speed", MenuOptionsScreen_Input, + EnvSettingsScreen_GetWeatherSpeed, EnvSettingsScreen_SetWeatherSpeed }, + { 1, 50, "Water level", MenuOptionsScreen_Input, + EnvSettingsScreen_GetEdgeHeight, EnvSettingsScreen_SetEdgeHeight } + }; + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchOptions); +} + +void EnvSettingsScreen_Show(void) { + MenuInput_Hex(menuOpts_descs[0], ENV_DEFAULT_CLOUDS_COLOR); + MenuInput_Hex(menuOpts_descs[1], ENV_DEFAULT_SKY_COLOR); + MenuInput_Hex(menuOpts_descs[2], ENV_DEFAULT_FOG_COLOR); + MenuInput_Float(menuOpts_descs[3], 0, 1000, 1); + MenuInput_Int(menuOpts_descs[4], -10000, 10000, World.Height + 2); + + MenuInput_Hex(menuOpts_descs[5], ENV_DEFAULT_SUN_COLOR); + MenuInput_Hex(menuOpts_descs[6], ENV_DEFAULT_SHADOW_COLOR); + MenuInput_Enum(menuOpts_descs[7], Weather_Names, Array_Elems(Weather_Names)); + MenuInput_Float(menuOpts_descs[8], -100, 100, 1); + MenuInput_Int(menuOpts_descs[9], -2048, 2048, World.Height / 2); + + MenuOptionsScreen_Show(EnvSettingsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*--------------------------------------------------GraphicsOptionsScreen--------------------------------------------------* +*#########################################################################################################################*/ +static void GraphicsOptionsScreen_CheckLightingModeAllowed(struct MenuOptionsScreen* s) { + Widget_SetDisabled(s->widgets[4], Lighting_ModeLockedByServer); +} + +static void GraphicsOptionsScreen_GetViewDist(cc_string* v) { String_AppendInt(v, Game_ViewDistance); } +static void GraphicsOptionsScreen_SetViewDist(const cc_string* v) { Game_UserSetViewDistance(Menu_Int(v)); } + +static void GraphicsOptionsScreen_GetSmooth(cc_string* v) { Menu_GetBool(v, Builder_SmoothLighting); } +static void GraphicsOptionsScreen_SetSmooth(const cc_string* v) { + Builder_SmoothLighting = Menu_SetBool(v, OPT_SMOOTH_LIGHTING); + Builder_ApplyActive(); + MapRenderer_Refresh(); +} + +static void GraphicsOptionsScreen_GetLighting(cc_string* v) { String_AppendConst(v, LightingMode_Names[Lighting_Mode]); } +static void GraphicsOptionsScreen_SetLighting(const cc_string* v) { + cc_uint8 mode = Utils_ParseEnum(v, 0, LightingMode_Names, LIGHTING_MODE_COUNT); + Options_Set(OPT_LIGHTING_MODE, v); + + Lighting_ModeSetByServer = false; + Lighting_SetMode(mode, false); +} + +static void GraphicsOptionsScreen_GetCamera(cc_string* v) { Menu_GetBool(v, Camera.Smooth); } +static void GraphicsOptionsScreen_SetCamera(const cc_string* v) { Camera.Smooth = Menu_SetBool(v, OPT_CAMERA_SMOOTH); } + +static void GraphicsOptionsScreen_GetNames(cc_string* v) { String_AppendConst(v, NameMode_Names[Entities.NamesMode]); } +static void GraphicsOptionsScreen_SetNames(const cc_string* v) { + Entities.NamesMode = Utils_ParseEnum(v, 0, NameMode_Names, NAME_MODE_COUNT); + Options_Set(OPT_NAMES_MODE, v); +} + +static void GraphicsOptionsScreen_GetShadows(cc_string* v) { String_AppendConst(v, ShadowMode_Names[Entities.ShadowsMode]); } +static void GraphicsOptionsScreen_SetShadows(const cc_string* v) { + Entities.ShadowsMode = Utils_ParseEnum(v, 0, ShadowMode_Names, SHADOW_MODE_COUNT); + Options_Set(OPT_ENTITY_SHADOW, v); +} + +static void GraphicsOptionsScreen_GetMipmaps(cc_string* v) { Menu_GetBool(v, Gfx.Mipmaps); } +static void GraphicsOptionsScreen_SetMipmaps(const cc_string* v) { + Gfx.Mipmaps = Menu_SetBool(v, OPT_MIPMAPS); + TexturePack_ExtractCurrent(true); +} + +static void GraphicsOptionsScreen_GetCameraMass(cc_string* v) { String_AppendFloat(v, Camera.Mass, 2); } +static void GraphicsOptionsScreen_SetCameraMass(const cc_string* c) { + Camera.Mass = Menu_Float(c); + Options_Set(OPT_CAMERA_MASS, c); +} + +static void GraphicsOptionsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -150, "Camera Mass", MenuOptionsScreen_Input, + GraphicsOptionsScreen_GetCameraMass, GraphicsOptionsScreen_SetCameraMass }, + { -1, -100, "FPS mode", MenuOptionsScreen_Enum, + MenuOptionsScreen_GetFPS, MenuOptionsScreen_SetFPS }, + { -1, -50, "View distance", MenuOptionsScreen_Input, + GraphicsOptionsScreen_GetViewDist, GraphicsOptionsScreen_SetViewDist }, + { -1, 0, "Smooth lighting", MenuOptionsScreen_Bool, + GraphicsOptionsScreen_GetSmooth, GraphicsOptionsScreen_SetSmooth }, + { -1, 50, "Lighting mode", MenuOptionsScreen_Enum, + GraphicsOptionsScreen_GetLighting, GraphicsOptionsScreen_SetLighting }, + { 1, -150, "Smooth camera", MenuOptionsScreen_Bool, + GraphicsOptionsScreen_GetCamera, GraphicsOptionsScreen_SetCamera }, + { 1, -100, "Names", MenuOptionsScreen_Enum, + GraphicsOptionsScreen_GetNames, GraphicsOptionsScreen_SetNames }, + { 1, -50, "Shadows", MenuOptionsScreen_Enum, + GraphicsOptionsScreen_GetShadows, GraphicsOptionsScreen_SetShadows }, +#ifdef CC_BUILD_N64 + { 1, 0, "Filtering", MenuOptionsScreen_Bool, + GraphicsOptionsScreen_GetMipmaps, GraphicsOptionsScreen_SetMipmaps }, +#else + { 1, 0, "Mipmaps", MenuOptionsScreen_Bool, + GraphicsOptionsScreen_GetMipmaps, GraphicsOptionsScreen_SetMipmaps }, +#endif + { 1, 50, "Anaglyph 3D", MenuOptionsScreen_Bool, + ClassicOptionsScreen_GetAnaglyph, ClassicOptionsScreen_SetAnaglyph } + }; + + s->OnLightingModeServerChanged = GraphicsOptionsScreen_CheckLightingModeAllowed; + + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchOptions); + GraphicsOptionsScreen_CheckLightingModeAllowed(s); + + s->descriptions[0] = "&eChange the smoothness of the smooth camera."; + s->descriptions[1] = \ + "&eVSync: &fNumber of frames rendered is at most the monitor's refresh rate.\n" \ + "&e30/60/120/144 FPS: &fRenders 30/60/120/144 frames at most each second.\n" \ + "&eNoLimit: &fRenders as many frames as possible each second.\n" \ + "&cNoLimit is pointless - it wastefully renders frames that you don't even see!"; + s->descriptions[3] = \ + "&eSmooth lighting smooths lighting and adds a minor glow to bright blocks.\n" \ + "&cNote: &eThis setting may reduce performance."; + s->descriptions[4] = \ + "&eClassic: &fTwo levels of light, sun and shadow.\n" \ + " Good for performance.\n" \ + "&eFancy: &fBright blocks cast a much wider range of light\n" \ + " May heavily reduce performance.\n" \ + "&cNote: &eIn multiplayer, this option may be changed or locked by the server."; + s->descriptions[6] = \ + "&eNone: &fNo names of players are drawn.\n" \ + "&eHovered: &fName of the targeted player is drawn see-through.\n" \ + "&eAll: &fNames of all other players are drawn normally.\n" \ + "&eAllHovered: &fAll names of players are drawn see-through.\n" \ + "&eAllUnscaled: &fAll names of players are drawn see-through without scaling."; + s->descriptions[7] = \ + "&eNone: &fNo entity shadows are drawn.\n" \ + "&eSnapToBlock: &fA square shadow is shown on block you are directly above.\n" \ + "&eCircle: &fA circular shadow is shown across the blocks you are above.\n" \ + "&eCircleAll: &fA circular shadow is shown underneath all entities."; +} + +void GraphicsOptionsScreen_Show(void) { + MenuInput_Float(menuOpts_descs[0], 1, 100, 20); + MenuInput_Enum(menuOpts_descs[1], FpsLimit_Names, FPS_LIMIT_COUNT); + MenuInput_Int(menuOpts_descs[2], 8, 4096, 512); + MenuInput_Enum(menuOpts_descs[4], LightingMode_Names, LIGHTING_MODE_COUNT) + MenuInput_Enum(menuOpts_descs[6], NameMode_Names, NAME_MODE_COUNT); + MenuInput_Enum(menuOpts_descs[7], ShadowMode_Names, SHADOW_MODE_COUNT); + + MenuOptionsScreen_Show(GraphicsOptionsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*----------------------------------------------------ChatOptionsScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static void ChatOptionsScreen_SetScale(const cc_string* v, float* target, const char* optKey) { + *target = Menu_Float(v); + Options_Set(optKey, v); + Gui_LayoutAll(); +} + +static void ChatOptionsScreen_GetAutoScaleChat(cc_string* v) { Menu_GetBool(v, Gui.AutoScaleChat); } +static void ChatOptionsScreen_SetAutoScaleChat(const cc_string* v) { + Gui.AutoScaleChat = Menu_SetBool(v, OPT_CHAT_AUTO_SCALE); + Gui_LayoutAll(); +} + +static void ChatOptionsScreen_GetChatScale(cc_string* v) { String_AppendFloat(v, Gui.RawChatScale, 1); } +static void ChatOptionsScreen_SetChatScale(const cc_string* v) { ChatOptionsScreen_SetScale(v, &Gui.RawChatScale, OPT_CHAT_SCALE); } + +static void ChatOptionsScreen_GetChatlines(cc_string* v) { String_AppendInt(v, Gui.Chatlines); } +static void ChatOptionsScreen_SetChatlines(const cc_string* v) { + Gui.Chatlines = Menu_Int(v); + ChatScreen_SetChatlines(Gui.Chatlines); + Options_Set(OPT_CHATLINES, v); +} + +static void ChatOptionsScreen_GetLogging(cc_string* v) { Menu_GetBool(v, Chat_Logging); } +static void ChatOptionsScreen_SetLogging(const cc_string* v) { + Chat_Logging = Menu_SetBool(v, OPT_CHAT_LOGGING); + if (!Chat_Logging) Chat_DisableLogging(); +} + +static void ChatOptionsScreen_GetClickable(cc_string* v) { Menu_GetBool(v, Gui.ClickableChat); } +static void ChatOptionsScreen_SetClickable(const cc_string* v) { Gui.ClickableChat = Menu_SetBool(v, OPT_CLICKABLE_CHAT); } + +static void ChatOptionsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, 0, "Chat scale", MenuOptionsScreen_Input, + ChatOptionsScreen_GetChatScale, ChatOptionsScreen_SetChatScale }, + { -1, 50, "Chat lines", MenuOptionsScreen_Input, + ChatOptionsScreen_GetChatlines, ChatOptionsScreen_SetChatlines }, + + { 1, 0, "Log to disk", MenuOptionsScreen_Bool, + ChatOptionsScreen_GetLogging, ChatOptionsScreen_SetLogging }, + { 1, 50, "Clickable chat", MenuOptionsScreen_Bool, + ChatOptionsScreen_GetClickable, ChatOptionsScreen_SetClickable }, + + { -1,-50, "Scale with window", MenuOptionsScreen_Bool, + ChatOptionsScreen_GetAutoScaleChat, ChatOptionsScreen_SetAutoScaleChat } + }; + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchOptions); +} + +void ChatOptionsScreen_Show(void) { + MenuInput_Float(menuOpts_descs[0], 0.25f, 4.00f, 1); + MenuInput_Int(menuOpts_descs[1], 0, 30, Gui.DefaultLines); + + MenuOptionsScreen_Show(ChatOptionsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*----------------------------------------------------GuiOptionsScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static void GuiOptionsScreen_GetShadows(cc_string* v) { Menu_GetBool(v, Drawer2D.BlackTextShadows); } +static void GuiOptionsScreen_SetShadows(const cc_string* v) { + Drawer2D.BlackTextShadows = Menu_SetBool(v, OPT_BLACK_TEXT); + Event_RaiseVoid(&ChatEvents.FontChanged); +} + +static void GuiOptionsScreen_GetShowFPS(cc_string* v) { Menu_GetBool(v, Gui.ShowFPS); } +static void GuiOptionsScreen_SetShowFPS(const cc_string* v) { Gui.ShowFPS = Menu_SetBool(v, OPT_SHOW_FPS); } + +static void GuiOptionsScreen_GetHotbar(cc_string* v) { String_AppendFloat(v, Gui.RawHotbarScale, 1); } +static void GuiOptionsScreen_SetHotbar(const cc_string* v) { ChatOptionsScreen_SetScale(v, &Gui.RawHotbarScale, OPT_HOTBAR_SCALE); } + +static void GuiOptionsScreen_GetInventory(cc_string* v) { String_AppendFloat(v, Gui.RawInventoryScale, 1); } +static void GuiOptionsScreen_SetInventory(const cc_string* v) { ChatOptionsScreen_SetScale(v, &Gui.RawInventoryScale, OPT_INVENTORY_SCALE); } + +static void GuiOptionsScreen_GetCrosshair(cc_string* v) { String_AppendFloat(v, Gui.RawCrosshairScale, 1); } +static void GuiOptionsScreen_SetCrosshair(const cc_string* v) { ChatOptionsScreen_SetScale(v, &Gui.RawCrosshairScale, OPT_CROSSHAIR_SCALE); } + +static void GuiOptionsScreen_GetTabAuto(cc_string* v) { Menu_GetBool(v, Gui.TabAutocomplete); } +static void GuiOptionsScreen_SetTabAuto(const cc_string* v) { Gui.TabAutocomplete = Menu_SetBool(v, OPT_TAB_AUTOCOMPLETE); } + +static void GuiOptionsScreen_GetUseFont(cc_string* v) { Menu_GetBool(v, !Drawer2D.BitmappedText); } +static void GuiOptionsScreen_SetUseFont(const cc_string* v) { + Drawer2D.BitmappedText = !Menu_SetBool(v, OPT_USE_CHAT_FONT); + Event_RaiseVoid(&ChatEvents.FontChanged); +} + +static void GuiOptionsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + + { -1, -100, "Show FPS", MenuOptionsScreen_Bool, + GuiOptionsScreen_GetShowFPS, GuiOptionsScreen_SetShowFPS }, + { -1, -50, "Hotbar scale", MenuOptionsScreen_Input, + GuiOptionsScreen_GetHotbar, GuiOptionsScreen_SetHotbar }, + { -1, 0, "Inventory scale", MenuOptionsScreen_Input, + GuiOptionsScreen_GetInventory, GuiOptionsScreen_SetInventory }, + { -1, 50, "Crosshair scale", MenuOptionsScreen_Input, + GuiOptionsScreen_GetCrosshair, GuiOptionsScreen_SetCrosshair }, + + { 1, -100, "Black text shadows", MenuOptionsScreen_Bool, + GuiOptionsScreen_GetShadows, GuiOptionsScreen_SetShadows }, + { 1, -50, "Tab auto-complete", MenuOptionsScreen_Bool, + GuiOptionsScreen_GetTabAuto, GuiOptionsScreen_SetTabAuto }, + { 1, 0, "Use system font", MenuOptionsScreen_Bool, + GuiOptionsScreen_GetUseFont, GuiOptionsScreen_SetUseFont }, + { 1, 50, "Select system font", Menu_SwitchFont, + NULL, NULL } + }; + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchOptions); +} + +void GuiOptionsScreen_Show(void) { + MenuInput_Float(menuOpts_descs[1], 0.25f, 4.00f, 1); + MenuInput_Float(menuOpts_descs[2], 0.25f, 4.00f, 1); + MenuInput_Float(menuOpts_descs[3], 0.25f, 4.00f, 1); + + MenuOptionsScreen_Show(GuiOptionsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*---------------------------------------------------HacksSettingsScreen---------------------------------------------------* +*#########################################################################################################################*/ +static void HacksSettingsScreen_GetHacks(cc_string* v) { Menu_GetBool(v, Entities.CurPlayer->Hacks.Enabled); } +static void HacksSettingsScreen_SetHacks(const cc_string* v) { + Entities.CurPlayer->Hacks.Enabled = Menu_SetBool(v,OPT_HACKS_ENABLED); + HacksComp_Update(&Entities.CurPlayer->Hacks); +} + +static void HacksSettingsScreen_GetSpeed(cc_string* v) { String_AppendFloat(v, Entities.CurPlayer->Hacks.SpeedMultiplier, 2); } +static void HacksSettingsScreen_SetSpeed(const cc_string* v) { + Entities.CurPlayer->Hacks.SpeedMultiplier = Menu_Float(v); + Options_Set(OPT_SPEED_FACTOR, v); +} + +static void HacksSettingsScreen_GetClipping(cc_string* v) { Menu_GetBool(v, Camera.Clipping); } +static void HacksSettingsScreen_SetClipping(const cc_string* v) { + Camera.Clipping = Menu_SetBool(v, OPT_CAMERA_CLIPPING); +} + +static void HacksSettingsScreen_GetJump(cc_string* v) { + String_AppendFloat(v, LocalPlayer_JumpHeight(Entities.CurPlayer), 3); +} + +static void HacksSettingsScreen_SetJump(const cc_string* v) { + cc_string str; char strBuffer[STRING_SIZE]; + struct PhysicsComp* physics; + + physics = &Entities.CurPlayer->Physics; + physics->JumpVel = PhysicsComp_CalcJumpVelocity(Menu_Float(v)); + physics->UserJumpVel = physics->JumpVel; + + String_InitArray(str, strBuffer); + String_AppendFloat(&str, physics->JumpVel, 8); + Options_Set(OPT_JUMP_VELOCITY, &str); +} + +static void HacksSettingsScreen_GetWOMHacks(cc_string* v) { Menu_GetBool(v, Entities.CurPlayer->Hacks.WOMStyleHacks); } +static void HacksSettingsScreen_SetWOMHacks(const cc_string* v) { + Entities.CurPlayer->Hacks.WOMStyleHacks = Menu_SetBool(v, OPT_WOM_STYLE_HACKS); +} + +static void HacksSettingsScreen_GetFullStep(cc_string* v) { Menu_GetBool(v, Entities.CurPlayer->Hacks.FullBlockStep); } +static void HacksSettingsScreen_SetFullStep(const cc_string* v) { + Entities.CurPlayer->Hacks.FullBlockStep = Menu_SetBool(v, OPT_FULL_BLOCK_STEP); +} + +static void HacksSettingsScreen_GetPushback(cc_string* v) { Menu_GetBool(v, Entities.CurPlayer->Hacks.PushbackPlacing); } +static void HacksSettingsScreen_SetPushback(const cc_string* v) { + Entities.CurPlayer->Hacks.PushbackPlacing = Menu_SetBool(v, OPT_PUSHBACK_PLACING); +} + +static void HacksSettingsScreen_GetLiquids(cc_string* v) { Menu_GetBool(v, Game_BreakableLiquids); } +static void HacksSettingsScreen_SetLiquids(const cc_string* v) { + Game_BreakableLiquids = Menu_SetBool(v, OPT_MODIFIABLE_LIQUIDS); +} + +static void HacksSettingsScreen_GetSlide(cc_string* v) { Menu_GetBool(v, Entities.CurPlayer->Hacks.NoclipSlide); } +static void HacksSettingsScreen_SetSlide(const cc_string* v) { + Entities.CurPlayer->Hacks.NoclipSlide = Menu_SetBool(v, OPT_NOCLIP_SLIDE); +} + +static void HacksSettingsScreen_GetFOV(cc_string* v) { String_AppendInt(v, Camera.Fov); } +static void HacksSettingsScreen_SetFOV(const cc_string* v) { + int fov = Menu_Int(v); + if (Camera.ZoomFov > fov) Camera.ZoomFov = fov; + Camera.DefaultFov = fov; + + Options_Set(OPT_FIELD_OF_VIEW, v); + Camera_SetFov(fov); +} + +static void HacksSettingsScreen_CheckHacksAllowed(struct MenuOptionsScreen* s) { + struct Widget** widgets = s->widgets; + struct LocalPlayer* p = Entities.CurPlayer; + cc_bool disabled = !p->Hacks.Enabled; + + Widget_SetDisabled(widgets[3], disabled || !p->Hacks.CanSpeed); + Widget_SetDisabled(widgets[4], disabled || !p->Hacks.CanSpeed); + Widget_SetDisabled(widgets[5], disabled || !p->Hacks.CanSpeed); + Widget_SetDisabled(widgets[7], disabled || !p->Hacks.CanPushbackBlocks); + MenuInputOverlay_CheckStillValid(s); +} + +static void HacksSettingsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -150, "Hacks enabled", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetHacks, HacksSettingsScreen_SetHacks }, + { -1, -100, "Speed multiplier", MenuOptionsScreen_Input, + HacksSettingsScreen_GetSpeed, HacksSettingsScreen_SetSpeed }, + { -1, -50, "Camera clipping", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetClipping, HacksSettingsScreen_SetClipping }, + { -1, 0, "Jump height", MenuOptionsScreen_Input, + HacksSettingsScreen_GetJump, HacksSettingsScreen_SetJump }, + { -1, 50, "WoM style hacks", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetWOMHacks, HacksSettingsScreen_SetWOMHacks }, + + { 1, -150, "Full block stepping", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetFullStep, HacksSettingsScreen_SetFullStep }, + { 1, -100, "Breakable liquids", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetLiquids, HacksSettingsScreen_SetLiquids }, + { 1, -50, "Pushback placing", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetPushback, HacksSettingsScreen_SetPushback }, + { 1, 0, "Noclip slide", MenuOptionsScreen_Bool, + HacksSettingsScreen_GetSlide, HacksSettingsScreen_SetSlide }, + { 1, 50, "Field of view", MenuOptionsScreen_Input, + HacksSettingsScreen_GetFOV, HacksSettingsScreen_SetFOV }, + }; + s->OnHacksChanged = HacksSettingsScreen_CheckHacksAllowed; + + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchOptions); + HacksSettingsScreen_CheckHacksAllowed(s); + + s->descriptions[2] = "&eIf &fON&e, then the third person cameras will limit\nðeir zoom distance if they hit a solid block."; + s->descriptions[3] = "&eSets how many blocks high you can jump up.\n&eNote: You jump much higher when holding down the Speed key binding."; + s->descriptions[4] = \ + "&eIf &fON&e, gives you a triple jump which increases speed massively,\n" \ + "&ealong with older noclip style. This is based on the \"World of Minecraft\"\n" \ + "&eclassic client mod, which popularized hacks conventions and controls\n" \ + "&ebefore ClassiCube was created."; + s->descriptions[7] = \ + "&eIf &fON&e, placing blocks that intersect your own position cause\n" \ + "ðe block to be placed, and you to be moved out of the way.\n" \ + "&fThis is mainly useful for quick pillaring/towering."; + s->descriptions[8] = "&eIf &fOFF&e, you will immediately stop when in noclip\n&emode and no movement keys are held down."; +} + +void HacksSettingsScreen_Show(void) { + MenuInput_Float(menuOpts_descs[1], 0.1f, 50, 10); + MenuInput_Float(menuOpts_descs[3], 0.1f, 2048, 1.233f); + MenuInput_Int(menuOpts_descs[9], 1, 179, 70); + + MenuOptionsScreen_Show(HacksSettingsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*----------------------------------------------------MiscOptionsScreen----------------------------------------------------* +*#########################################################################################################################*/ +static void MiscOptionsScreen_GetReach(cc_string* v) { String_AppendFloat(v, Entities.CurPlayer->ReachDistance, 2); } +static void MiscOptionsScreen_SetReach(const cc_string* v) { Entities.CurPlayer->ReachDistance = Menu_Float(v); } + +static void MiscOptionsScreen_GetMusic(cc_string* v) { String_AppendInt(v, Audio_MusicVolume); } +static void MiscOptionsScreen_SetMusic(const cc_string* v) { + Options_Set(OPT_MUSIC_VOLUME, v); + Audio_SetMusic(Menu_Int(v)); +} + +static void MiscOptionsScreen_GetSounds(cc_string* v) { String_AppendInt(v, Audio_SoundsVolume); } +static void MiscOptionsScreen_SetSounds(const cc_string* v) { + Options_Set(OPT_SOUND_VOLUME, v); + Audio_SetSounds(Menu_Int(v)); +} + +static void MiscOptionsScreen_GetViewBob(cc_string* v) { Menu_GetBool(v, Game_ViewBobbing); } +static void MiscOptionsScreen_SetViewBob(const cc_string* v) { Game_ViewBobbing = Menu_SetBool(v, OPT_VIEW_BOBBING); } + +static void MiscOptionsScreen_GetPhysics(cc_string* v) { Menu_GetBool(v, Physics.Enabled); } +static void MiscOptionsScreen_SetPhysics(const cc_string* v) { + Physics_SetEnabled(Menu_SetBool(v, OPT_BLOCK_PHYSICS)); +} + +static void MiscOptionsScreen_GetInvert(cc_string* v) { Menu_GetBool(v, Camera.Invert); } +static void MiscOptionsScreen_SetInvert(const cc_string* v) { Camera.Invert = Menu_SetBool(v, OPT_INVERT_MOUSE); } + +static void MiscOptionsScreen_GetSensitivity(cc_string* v) { String_AppendInt(v, Camera.Sensitivity); } +static void MiscOptionsScreen_SetSensitivity(const cc_string* v) { + Camera.Sensitivity = Menu_Int(v); + Options_Set(OPT_SENSITIVITY, v); +} + +static void MiscSettingsScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -100, "Reach distance", MenuOptionsScreen_Input, + MiscOptionsScreen_GetReach, MiscOptionsScreen_SetReach }, + { -1, -50, "Music volume", MenuOptionsScreen_Input, + MiscOptionsScreen_GetMusic, MiscOptionsScreen_SetMusic }, + { -1, 0, "Sounds volume", MenuOptionsScreen_Input, + MiscOptionsScreen_GetSounds, MiscOptionsScreen_SetSounds }, + { -1, 50, "View bobbing", MenuOptionsScreen_Bool, + MiscOptionsScreen_GetViewBob, MiscOptionsScreen_SetViewBob }, + + { 1, -100, "Block physics", MenuOptionsScreen_Bool, + MiscOptionsScreen_GetPhysics, MiscOptionsScreen_SetPhysics }, + { 1, 0, "Invert mouse", MenuOptionsScreen_Bool, + MiscOptionsScreen_GetInvert, MiscOptionsScreen_SetInvert }, + { 1, 50, "Mouse sensitivity", MenuOptionsScreen_Input, + MiscOptionsScreen_GetSensitivity, MiscOptionsScreen_SetSensitivity } + }; + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchOptions); + + /* Disable certain options */ + if (!Server.IsSinglePlayer) Menu_Remove(s, 0); + if (!Server.IsSinglePlayer) Menu_Remove(s, 4); +} + +void MiscOptionsScreen_Show(void) { + MenuInput_Float(menuOpts_descs[0], 1, 1024, 5); + MenuInput_Int(menuOpts_descs[1], 0, 100, DEFAULT_MUSIC_VOLUME); + MenuInput_Int(menuOpts_descs[2], 0, 100, DEFAULT_SOUNDS_VOLUME); +#ifdef CC_BUILD_WIN + MenuInput_Int(menuOpts_descs[6], 1, 200, 40); +#else + MenuInput_Int(menuOpts_descs[6], 1, 200, 30); +#endif + + MenuOptionsScreen_Show(MiscSettingsScreen_InitWidgets); +} + + +/*########################################################################################################################* +*--------------------------------------------------NostalgiaMenuScreen-----------------------------------------------------* +*#########################################################################################################################*/ +static struct NostalgiaMenuScreen { + Screen_Body + struct ButtonWidget btnA, btnF, done; + struct TextWidget title; +} NostalgiaMenuScreen; + +static struct Widget* nostalgiaMenu_widgets[4]; + +static void NostalgiaMenuScreen_Appearance(void* a, void* b) { NostalgiaAppearanceScreen_Show(); } +static void NostalgiaMenuScreen_Functionality(void* a, void* b) { NostalgiaFunctionalityScreen_Show(); } + +static void NostalgiaMenuScreen_SwitchBack(void* a, void* b) { + if (Gui.ClassicMenu) { Menu_SwitchPause(a, b); } else { Menu_SwitchOptions(a, b); } +} + +static void NostalgiaMenuScreen_ContextRecreated(void* screen) { + struct NostalgiaMenuScreen* s = (struct NostalgiaMenuScreen*)screen; + struct FontDesc titleFont; + Screen_UpdateVb(screen); + Gui_MakeTitleFont(&titleFont); + + TextWidget_SetConst(&s->title, "Nostalgia options", &titleFont); + ButtonWidget_SetConst(&s->btnA, "Appearance", &titleFont); + ButtonWidget_SetConst(&s->btnF, "Functionality", &titleFont); + ButtonWidget_SetConst(&s->done, "Done", &titleFont); + + Font_Free(&titleFont); +} + +static void NostalgiaMenuScreen_Layout(void* screen) { + struct NostalgiaMenuScreen* s = (struct NostalgiaMenuScreen*)screen; + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -100); + Widget_SetLocation(&s->btnA, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -25); + Widget_SetLocation(&s->btnF, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 25); + Menu_LayoutBack(&s->done); +} + +static void NostalgiaMenuScreen_Init(void* screen) { + struct NostalgiaMenuScreen* s = (struct NostalgiaMenuScreen*)screen; + + s->widgets = nostalgiaMenu_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(nostalgiaMenu_widgets); + + ButtonWidget_Add(s, &s->btnA, 400, NostalgiaMenuScreen_Appearance); + ButtonWidget_Add(s, &s->btnF, 400, NostalgiaMenuScreen_Functionality); + ButtonWidget_Add(s, &s->done, 400, NostalgiaMenuScreen_SwitchBack); + TextWidget_Add(s, &s->title); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE NostalgiaMenuScreen_VTABLE = { + NostalgiaMenuScreen_Init, Screen_NullUpdate, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + NostalgiaMenuScreen_Layout, Screen_ContextLost, NostalgiaMenuScreen_ContextRecreated +}; +void NostalgiaMenuScreen_Show(void) { + struct NostalgiaMenuScreen* s = &NostalgiaMenuScreen; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &NostalgiaMenuScreen_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_MENU); +} + + +/*########################################################################################################################* +*------------------------------------------------NostalgiaAppearanceScreen------------------------------------------------* +*#########################################################################################################################*/ +static void NostalgiaScreen_GetHand(cc_string* v) { Menu_GetBool(v, Models.ClassicArms); } +static void NostalgiaScreen_SetHand(const cc_string* v) { Models.ClassicArms = Menu_SetBool(v, OPT_CLASSIC_ARM_MODEL); } + +static void NostalgiaScreen_GetAnim(cc_string* v) { Menu_GetBool(v, !Game_SimpleArmsAnim); } +static void NostalgiaScreen_SetAnim(const cc_string* v) { + Game_SimpleArmsAnim = String_CaselessEqualsConst(v, "OFF"); + Options_SetBool(OPT_SIMPLE_ARMS_ANIM, Game_SimpleArmsAnim); +} + +static void NostalgiaScreen_GetClassicChat(cc_string* v) { Menu_GetBool(v, Gui.ClassicChat); } +static void NostalgiaScreen_SetClassicChat(const cc_string* v) { Gui.ClassicChat = Menu_SetBool(v, OPT_CLASSIC_CHAT); } + +static void NostalgiaScreen_GetClassicInv(cc_string* v) { Menu_GetBool(v, Gui.ClassicInventory); } +static void NostalgiaScreen_SetClassicInv(const cc_string* v) { Gui.ClassicInventory = Menu_SetBool(v, OPT_CLASSIC_INVENTORY); } + +static void NostalgiaScreen_GetGui(cc_string* v) { Menu_GetBool(v, Gui.ClassicTexture); } +static void NostalgiaScreen_SetGui(const cc_string* v) { Gui.ClassicTexture = Menu_SetBool(v, OPT_CLASSIC_GUI); } + +static void NostalgiaScreen_GetList(cc_string* v) { Menu_GetBool(v, Gui.ClassicTabList); } +static void NostalgiaScreen_SetList(const cc_string* v) { Gui.ClassicTabList = Menu_SetBool(v, OPT_CLASSIC_TABLIST); } + +static void NostalgiaScreen_GetOpts(cc_string* v) { Menu_GetBool(v, Gui.ClassicMenu); } +static void NostalgiaScreen_SetOpts(const cc_string* v) { Gui.ClassicMenu = Menu_SetBool(v, OPT_CLASSIC_OPTIONS); } + +static void NostalgiaAppearanceScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -100, "Classic hand model", MenuOptionsScreen_Bool, + NostalgiaScreen_GetHand, NostalgiaScreen_SetHand }, + { -1, -50, "Classic walk anim", MenuOptionsScreen_Bool, + NostalgiaScreen_GetAnim, NostalgiaScreen_SetAnim }, + { -1, 0, "Classic chat", MenuOptionsScreen_Bool, + NostalgiaScreen_GetClassicChat, NostalgiaScreen_SetClassicChat }, + { -1, 50, "Classic inventory", MenuOptionsScreen_Bool, + NostalgiaScreen_GetClassicInv, NostalgiaScreen_SetClassicInv }, + { 1, -50, "Classic GUI textures", MenuOptionsScreen_Bool, + NostalgiaScreen_GetGui, NostalgiaScreen_SetGui }, + { 1, 0, "Classic player list", MenuOptionsScreen_Bool, + NostalgiaScreen_GetList, NostalgiaScreen_SetList }, + { 1, 50, "Classic options", MenuOptionsScreen_Bool, + NostalgiaScreen_GetOpts, NostalgiaScreen_SetOpts }, + }; + + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchNostalgia); +} + +void NostalgiaAppearanceScreen_Show(void) { + MenuOptionsScreen_Show(NostalgiaAppearanceScreen_InitWidgets); +} + + +/*########################################################################################################################* +*----------------------------------------------NostalgiaFunctionalityScreen-----------------------------------------------* +*#########################################################################################################################*/ +static void NostalgiaScreen_UpdateVersionDisabled(void) { + struct ButtonWidget* gameVerBtn = &MenuOptionsScreen_Instance.buttons[3]; + Widget_SetDisabled(gameVerBtn, Game_Version.HasCPE); +} + +static void NostalgiaScreen_GetTexs(cc_string* v) { Menu_GetBool(v, Game_AllowServerTextures); } +static void NostalgiaScreen_SetTexs(const cc_string* v) { Game_AllowServerTextures = Menu_SetBool(v, OPT_SERVER_TEXTURES); } + +static void NostalgiaScreen_GetCustom(cc_string* v) { Menu_GetBool(v, Game_AllowCustomBlocks); } +static void NostalgiaScreen_SetCustom(const cc_string* v) { Game_AllowCustomBlocks = Menu_SetBool(v, OPT_CUSTOM_BLOCKS); } + +static void NostalgiaScreen_GetCPE(cc_string* v) { Menu_GetBool(v, Game_Version.HasCPE); } +static void NostalgiaScreen_SetCPE(const cc_string* v) { + Menu_SetBool(v, OPT_CPE); + GameVersion_Load(); + NostalgiaScreen_UpdateVersionDisabled(); +} + +static void NostalgiaScreen_Version(void* screen, void* widget) { + struct MenuOptionsScreen* s = (struct MenuOptionsScreen*)screen; + int ver = Game_Version.Version - 1; + if (ver < VERSION_0017) ver = VERSION_0030; + + Options_SetInt(OPT_GAME_VERSION, ver); + GameVersion_Load(); + MenuOptionsScreen_Update(s, widget); +} + +static void NostalgiaScreen_GetVersion(cc_string* v) { String_AppendConst(v, Game_Version.Name); } +static void NostalgiaScreen_SetVersion(const cc_string* v) { } + +static struct TextWidget nostalgia_desc; +static void NostalgiaScreen_RecreateExtra(struct MenuOptionsScreen* s) { + TextWidget_SetConst(&nostalgia_desc, "&eRequires restarting game to take full effect", &s->textFont); +} + +static void NostalgiaFunctionalityScreen_InitWidgets(struct MenuOptionsScreen* s) { + static const struct MenuOptionDesc buttons[] = { + { -1, -50, "Use server textures", MenuOptionsScreen_Bool, + NostalgiaScreen_GetTexs, NostalgiaScreen_SetTexs }, + { -1, 0, "Allow custom blocks", MenuOptionsScreen_Bool, + NostalgiaScreen_GetCustom, NostalgiaScreen_SetCustom }, + + { 1, -50, "Non-classic features", MenuOptionsScreen_Bool, + NostalgiaScreen_GetCPE, NostalgiaScreen_SetCPE }, + { 1, 0, "Game version", NostalgiaScreen_Version, + NostalgiaScreen_GetVersion, NostalgiaScreen_SetVersion } + }; + s->DoRecreateExtra = NostalgiaScreen_RecreateExtra; + + MenuOptionsScreen_AddButtons(s, buttons, Array_Elems(buttons), Menu_SwitchNostalgia); + TextWidget_Add(s, &nostalgia_desc); + Widget_SetLocation(&nostalgia_desc, ANCHOR_CENTRE, ANCHOR_CENTRE, 0, 100); + + NostalgiaScreen_UpdateVersionDisabled(); + s->descriptions[3] = \ + "&eNote that support for versions earlier than 0.30 is incomplete.\n" \ + "\n" \ + "&cNote that some servers only support 0.30 game version"; +} + +void NostalgiaFunctionalityScreen_Show(void) { + MenuOptionsScreen_Show(NostalgiaFunctionalityScreen_InitWidgets); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Overlay---------------------------------------------------------* +*#########################################################################################################################*/ +static void Overlay_AddLabels(void* screen, struct TextWidget* labels) { + int i; + TextWidget_Add(screen, &labels[0]); + for (i = 1; i < 4; i++) + { + TextWidget_Add(screen, &labels[i]); + labels[i].color = PackedCol_Make(224, 224, 224, 255); + } +} + +static void Overlay_LayoutLabels(struct TextWidget* labels) { + int i; + Widget_SetLocation(&labels[0], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -120); + for (i = 1; i < 4; i++) { + Widget_SetLocation(&labels[i], ANCHOR_CENTRE, ANCHOR_CENTRE, 0, -70 + 20 * i); + } +} + +static void Overlay_LayoutMainButtons(struct ButtonWidget* btns) { + Widget_SetLocation(&btns[0], ANCHOR_CENTRE, ANCHOR_CENTRE, -110, 30); + Widget_SetLocation(&btns[1], ANCHOR_CENTRE, ANCHOR_CENTRE, 110, 30); +} + + +/*########################################################################################################################* +*------------------------------------------------------TexIdsOverlay------------------------------------------------------* +*#########################################################################################################################*/ +static struct TexIdsOverlay { + Screen_Body + int xOffset, yOffset, tileSize, textVertices; + struct TextAtlas idAtlas; + struct TextWidget title; +} TexIdsOverlay; +static struct Widget* texids_widgets[1]; + +#define TEXIDS_MAX_ROWS_PER_PAGE 16 +#define TEXIDS_MAX_PER_PAGE (TEXIDS_MAX_ROWS_PER_PAGE * ATLAS2D_TILES_PER_ROW) +#define TEXIDS_TEXT_VERTICES (10 * 4 + 90 * 8 + 412 * 12) /* '0'-'9' + '10'-'99' + '100'-'511' */ +#define TEXIDS_MAX_VERTICES (TEXTWIDGET_MAX + 4 * ATLAS1D_MAX_ATLASES + TEXIDS_TEXT_VERTICES) + +static void TexIdsOverlay_Layout(void* screen) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + int size; + + size = Window_UI.Height / ATLAS2D_TILES_PER_ROW; + size = (size / 8) * 8; + Math_Clamp(size, 8, 40); + + s->xOffset = Gui_CalcPos(ANCHOR_CENTRE, 0, size * Atlas2D.RowsCount, Window_UI.Width); + s->yOffset = Gui_CalcPos(ANCHOR_CENTRE, 0, size * ATLAS2D_TILES_PER_ROW, Window_UI.Height); + s->tileSize = size; + + /* Can't use vertical centreing here */ + Widget_SetLocation(&s->title, ANCHOR_CENTRE, ANCHOR_MIN, 0, 0); + s->title.yOffset = s->yOffset - Display_ScaleY(30); + Widget_Layout(&s->title); +} + +static void TexIdsOverlay_ContextLost(void* screen) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + Screen_ContextLost(s); + TextAtlas_Free(&s->idAtlas); +} + +static void TexIdsOverlay_ContextRecreated(void* screen) { + static const cc_string chars = String_FromConst("0123456789"); + static const cc_string prefix = String_FromConst("f"); + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + struct FontDesc textFont, titleFont; + + Screen_UpdateVb(screen); + Font_Make(&textFont, 8, FONT_FLAGS_PADDING); + Font_SetPadding(&textFont, 1); + TextAtlas_Make(&s->idAtlas, &chars, &textFont, &prefix); + Font_Free(&textFont); + + Gui_MakeTitleFont(&titleFont); + TextWidget_SetConst(&s->title, "Texture ID reference sheet", &titleFont); + Font_Free(&titleFont); +} + +static void TexIdsOverlay_BuildTerrain(struct TexIdsOverlay* s, struct VertexTextured** ptr) { + struct Texture tex; + int baseLoc, xOffset; + int i, row, size; + + size = s->tileSize; + baseLoc = 0; + xOffset = s->xOffset; + + tex.uv.u1 = 0.0f; tex.uv.u2 = UV2_Scale; + tex.width = size; tex.height = size; + + for (row = 0; row < Atlas2D.RowsCount; row += TEXIDS_MAX_ROWS_PER_PAGE) { + for (i = 0; i < TEXIDS_MAX_PER_PAGE; i++) { + + tex.x = xOffset + Atlas2D_TileX(i) * size; + tex.y = s->yOffset + Atlas2D_TileY(i) * size; + + tex.uv.v1 = Atlas1D_RowId(i + baseLoc) * Atlas1D.InvTileSize; + tex.uv.v2 = tex.uv.v1 + UV2_Scale * Atlas1D.InvTileSize; + + Gfx_Make2DQuad(&tex, PACKEDCOL_WHITE, ptr); + } + + baseLoc += TEXIDS_MAX_PER_PAGE; + xOffset += size * ATLAS2D_TILES_PER_ROW; + } +} + +static void TexIdsOverlay_BuildText(struct TexIdsOverlay* s, struct VertexTextured** ptr) { + struct TextAtlas* idAtlas; + struct VertexTextured* beg; + int xOffset, size, row; + int x, y, id = 0; + + size = s->tileSize; + xOffset = s->xOffset; + idAtlas = &s->idAtlas; + beg = *ptr; + + for (row = 0; row < Atlas2D.RowsCount; row += TEXIDS_MAX_ROWS_PER_PAGE) { + idAtlas->tex.y = s->yOffset + (size - idAtlas->tex.height); + + for (y = 0; y < ATLAS2D_TILES_PER_ROW; y++) { + for (x = 0; x < ATLAS2D_TILES_PER_ROW; x++) { + idAtlas->curX = xOffset + size * x + 3; /* offset text by 3 pixels */ + TextAtlas_AddInt(idAtlas, id++, ptr); + } + idAtlas->tex.y += size; + } + xOffset += size * ATLAS2D_TILES_PER_ROW; + } + s->textVertices = (int)(*ptr - beg); +} + +static void TexIdsOverlay_BuildMesh(void* screen) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + struct VertexTextured* data; + struct VertexTextured** ptr; + + data = Screen_LockVb(s); + ptr = &data; + + Widget_BuildMesh(&s->title, ptr); + TexIdsOverlay_BuildTerrain(s, ptr); + TexIdsOverlay_BuildText(s, ptr); + Gfx_UnlockDynamicVb(s->vb); +} + +static int TexIdsOverlay_RenderTerrain(struct TexIdsOverlay* s, int offset) { + int i, count = Atlas1D.TilesPerAtlas * 4; + for (i = 0; i < Atlas1D.Count; i++) { + Gfx_BindTexture(Atlas1D.TexIds[i]); + + Gfx_DrawVb_IndexedTris_Range(count, offset); + offset += count; + } + return offset; +} + +static void TexIdsOverlay_OnAtlasChanged(void* screen) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + s->dirty = true; + /* Atlas may have 256 or 512 textures, which changes s->xOffset */ + /* This can resize the position of the 'pages', so just re-layout */ + TexIdsOverlay_Layout(screen); +} + +static void TexIdsOverlay_Init(void* screen) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + s->widgets = texids_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(texids_widgets); + s->maxVertices = TEXIDS_MAX_VERTICES; + + TextWidget_Add(s, &s->title); + Event_Register_(&TextureEvents.AtlasChanged, s, TexIdsOverlay_OnAtlasChanged); +} + +static void TexIdsOverlay_Free(void* screen) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + Event_Unregister_(&TextureEvents.AtlasChanged, s, TexIdsOverlay_OnAtlasChanged); +} + +static void TexIdsOverlay_Render(void* screen, float delta) { + struct TexIdsOverlay* s = (struct TexIdsOverlay*)screen; + int offset = 0; + Menu_RenderBounds(); + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Gfx_BindDynamicVb(s->vb); + + offset = Widget_Render2(&s->title, offset); + offset = TexIdsOverlay_RenderTerrain(s, offset); + + Gfx_BindTexture(s->idAtlas.tex.ID); + Gfx_DrawVb_IndexedTris_Range(s->textVertices, offset); +} + +static int TexIdsOverlay_KeyDown(void* screen, int key) { + struct Screen* s = (struct Screen*)screen; + if (InputBind_Claims(BIND_IDOVERLAY, key)) { Gui_Remove(s); return true; } + return false; +} + +static const struct ScreenVTABLE TexIdsOverlay_VTABLE = { + TexIdsOverlay_Init, Screen_NullUpdate, TexIdsOverlay_Free, + TexIdsOverlay_Render, TexIdsOverlay_BuildMesh, + TexIdsOverlay_KeyDown, Screen_InputUp, Screen_FKeyPress, Screen_FText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + TexIdsOverlay_Layout, TexIdsOverlay_ContextLost, TexIdsOverlay_ContextRecreated +}; +void TexIdsOverlay_Show(void) { + struct TexIdsOverlay* s = &TexIdsOverlay; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &TexIdsOverlay_VTABLE; + Gui_Add((struct Screen*)s, GUI_PRIORITY_TEXIDS); +} + + +/*########################################################################################################################* +*----------------------------------------------------UrlWarningOverlay----------------------------------------------------* +*#########################################################################################################################*/ +static struct UrlWarningOverlay { + Screen_Body + cc_string url; + struct ButtonWidget btns[2]; + struct TextWidget lbls[4]; + char _urlBuffer[STRING_SIZE * 4]; +} UrlWarningOverlay; + +static struct Widget* urlwarning_widgets[4 + 2]; + +static void UrlWarningOverlay_OpenUrl(void* screen, void* b) { + struct UrlWarningOverlay* s = (struct UrlWarningOverlay*)screen; + cc_result res = Process_StartOpen(&s->url); + if (res) Logger_SimpleWarn2(res, "opening url in browser", &s->url); + Gui_Remove((struct Screen*)s); +} + +static void UrlWarningOverlay_AppendUrl(void* screen, void* b) { + struct UrlWarningOverlay* s = (struct UrlWarningOverlay*)screen; + if (Gui.ClickableChat) ChatScreen_AppendInput(&s->url); + Gui_Remove((struct Screen*)s); +} + +static void UrlWarningOverlay_ContextRecreated(void* screen) { + struct UrlWarningOverlay* s = (struct UrlWarningOverlay*)screen; + struct FontDesc titleFont, textFont; + Screen_UpdateVb(screen); + + Gui_MakeTitleFont(&titleFont); + Gui_MakeBodyFont(&textFont); + + TextWidget_SetConst(&s->lbls[0], "&eAre you sure you want to open this link?", &titleFont); + TextWidget_Set(&s->lbls[1], &s->url, &textFont); + TextWidget_SetConst(&s->lbls[2], "Be careful - links from strangers may be websites that", &textFont); + TextWidget_SetConst(&s->lbls[3], " have viruses, or things you may not want to open/see.", &textFont); + + ButtonWidget_SetConst(&s->btns[0], "Yes", &titleFont); + ButtonWidget_SetConst(&s->btns[1], "No", &titleFont); + Font_Free(&titleFont); + Font_Free(&textFont); +} + +static void UrlWarningOverlay_Layout(void* screen) { + struct UrlWarningOverlay* s = (struct UrlWarningOverlay*)screen; + Overlay_LayoutLabels(s->lbls); + Overlay_LayoutMainButtons(s->btns); +} + +static void UrlWarningOverlay_Init(void* screen) { + struct UrlWarningOverlay* s = (struct UrlWarningOverlay*)screen; + s->widgets = urlwarning_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(urlwarning_widgets); + + Overlay_AddLabels(s, s->lbls); + ButtonWidget_Add(s, &s->btns[0], 160, UrlWarningOverlay_OpenUrl); + ButtonWidget_Add(s, &s->btns[1], 160, UrlWarningOverlay_AppendUrl); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE UrlWarningOverlay_VTABLE = { + UrlWarningOverlay_Init, Screen_NullUpdate, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + UrlWarningOverlay_Layout, Screen_ContextLost, UrlWarningOverlay_ContextRecreated +}; +void UrlWarningOverlay_Show(const cc_string* url) { + struct UrlWarningOverlay* s = &UrlWarningOverlay; + s->grabsInput = true; + s->closable = true; + s->VTABLE = &UrlWarningOverlay_VTABLE; + + String_InitArray(s->url, s->_urlBuffer); + String_Copy(&s->url, url); + Gui_Add((struct Screen*)s, GUI_PRIORITY_URLWARNING); +} + + +/*########################################################################################################################* +*-----------------------------------------------------TexPackOverlay------------------------------------------------------* +*#########################################################################################################################*/ +static struct TexPackOverlay { + Screen_Body + cc_bool deny, alwaysDeny, gotContent; + cc_uint32 contentLength; + cc_string url; + int reqID; + struct FontDesc textFont; + struct ButtonWidget btns[4]; + struct TextWidget lbls[4]; + char _urlBuffer[URL_MAX_SIZE]; +} TexPackOverlay; + +static struct Widget* texpack_widgets[4 + 4]; + +static cc_bool TexPackOverlay_IsAlways(void* screen, void* w) { + struct ButtonWidget* btn = (struct ButtonWidget*)w; + return btn->meta.val != 0; +} + +static void TexPackOverlay_YesClick(void* screen, void* widget) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + TexturePack_Extract(&s->url); + if (TexPackOverlay_IsAlways(s, widget)) TextureCache_Accept(&s->url); + Gui_Remove((struct Screen*)s); +} + +static void TexPackOverlay_NoClick(void* screen, void* widget) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + s->alwaysDeny = TexPackOverlay_IsAlways(s, widget); + s->deny = true; + Gui_Refresh((struct Screen*)s); +} + +static void TexPackOverlay_ConfirmNoClick(void* screen, void* b) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + if (s->alwaysDeny) TextureCache_Deny(&s->url); + Gui_Remove((struct Screen*)s); +} + +static void TexPackOverlay_GoBackClick(void* screen, void* b) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + s->deny = false; + Gui_Refresh((struct Screen*)s); +} + +static void TexPackOverlay_UpdateLine2(struct TexPackOverlay* s) { + static const cc_string https = String_FromConst("https://"); + static const cc_string http = String_FromConst("http://"); + cc_string url = String_Empty; + + if (!s->deny) { + url = s->url; + if (String_CaselessStarts(&url, &https)) { + url = String_UNSAFE_SubstringAt(&url, https.length); + } + if (String_CaselessStarts(&url, &http)) { + url = String_UNSAFE_SubstringAt(&url, http.length); + } + } + TextWidget_Set(&s->lbls[2], &url, &s->textFont); +} + +static void TexPackOverlay_UpdateLine3(struct TexPackOverlay* s) { + cc_string contents; char contentsBuffer[STRING_SIZE]; + float contentLengthMB; + + if (s->deny) { + TextWidget_SetConst(&s->lbls[3], "Sure you don't want to download the texture pack?", &s->textFont); + } else if (s->contentLength) { + String_InitArray(contents, contentsBuffer); + contentLengthMB = s->contentLength / (1024.0f * 1024.0f); + String_Format1(&contents, "Download size: %f3 MB", &contentLengthMB); + TextWidget_Set(&s->lbls[3], &contents, &s->textFont); + } else if (s->gotContent) { + TextWidget_SetConst(&s->lbls[3], "Download size: Unknown", &s->textFont); + } else { + TextWidget_SetConst(&s->lbls[3], "Download size: Determining...", &s->textFont); + } +} + +static void TexPackOverlay_Update(void* screen, float delta) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + struct HttpRequest item; + if (!Http_GetResult(s->reqID, &item)) return; + + s->dirty = true; + s->gotContent = true; + s->contentLength = item.contentLength; + + TexPackOverlay_UpdateLine3(s); + HttpRequest_Free(&item); +} + +static void TexPackOverlay_ContextLost(void* screen) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + Font_Free(&s->textFont); + Screen_ContextLost(screen); +} + +static void TexPackOverlay_ContextRecreated(void* screen) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + struct FontDesc titleFont; + Screen_UpdateVb(screen); + + Gui_MakeTitleFont(&titleFont); + Gui_MakeBodyFont(&s->textFont); + + TextWidget_SetConst(&s->lbls[0], s->deny ? "&eYou might be missing out." + : "Do you want to download the server's texture pack?", &titleFont); + TextWidget_SetConst(&s->lbls[1], !s->deny ? "Texture pack url:" + : "Texture packs can play a vital role in the look and feel of maps.", &s->textFont); + TexPackOverlay_UpdateLine2(s); + TexPackOverlay_UpdateLine3(s); + + ButtonWidget_SetConst(&s->btns[0], s->deny ? "I'm sure" : "Yes", &titleFont); + ButtonWidget_SetConst(&s->btns[1], s->deny ? "Go back" : "No", &titleFont); + s->btns[0].MenuClick = s->deny ? TexPackOverlay_ConfirmNoClick : TexPackOverlay_YesClick; + s->btns[1].MenuClick = s->deny ? TexPackOverlay_GoBackClick : TexPackOverlay_NoClick; + + if (!s->deny) { + ButtonWidget_SetConst(&s->btns[2], "Always yes", &titleFont); + ButtonWidget_SetConst(&s->btns[3], "Always no", &titleFont); + s->btns[2].MenuClick = TexPackOverlay_YesClick; + s->btns[3].MenuClick = TexPackOverlay_NoClick; + s->btns[2].meta.val = 1; + s->btns[3].meta.val = 1; + } + + s->numWidgets = s->deny ? 6 : 8; + Font_Free(&titleFont); +} + +static void TexPackOverlay_Layout(void* screen) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + Overlay_LayoutLabels(s->lbls); + Overlay_LayoutMainButtons(s->btns); + Widget_SetLocation(&s->btns[2], ANCHOR_CENTRE, ANCHOR_CENTRE, -110, 85); + Widget_SetLocation(&s->btns[3], ANCHOR_CENTRE, ANCHOR_CENTRE, 110, 85); +} + +static void TexPackOverlay_Init(void* screen) { + struct TexPackOverlay* s = (struct TexPackOverlay*)screen; + s->widgets = texpack_widgets; + s->numWidgets = 0; + s->maxWidgets = Array_Elems(texpack_widgets); + + s->contentLength = 0; + s->gotContent = false; + s->deny = false; + Overlay_AddLabels(s, s->lbls); + + ButtonWidget_Add(s, &s->btns[0], 160, NULL); + ButtonWidget_Add(s, &s->btns[1], 160, NULL); + ButtonWidget_Add(s, &s->btns[2], 160, NULL); + ButtonWidget_Add(s, &s->btns[3], 160, NULL); + + s->maxVertices = Screen_CalcDefaultMaxVertices(s); +} + +static const struct ScreenVTABLE TexPackOverlay_VTABLE = { + TexPackOverlay_Init, TexPackOverlay_Update, Screen_NullFunc, + MenuScreen_Render2, Screen_BuildMesh, + Menu_InputDown, Screen_InputUp, Screen_TKeyPress, Screen_TText, + Menu_PointerDown, Screen_PointerUp, Menu_PointerMove, Screen_TMouseScroll, + TexPackOverlay_Layout, TexPackOverlay_ContextLost, TexPackOverlay_ContextRecreated +}; +void TexPackOverlay_Show(const cc_string* url) { + struct TexPackOverlay* s = &TexPackOverlay; + s->grabsInput = true; + /* Too easy to accidentally ESC this important dialog */ + /* s->closable= true; */ + s->VTABLE = &TexPackOverlay_VTABLE; + + String_InitArray(s->url, s->_urlBuffer); + String_Copy(&s->url, url); + + s->reqID = Http_AsyncGetHeaders(url, HTTP_FLAG_PRIORITY); + Gui_Add((struct Screen*)s, GUI_PRIORITY_TEXPACK); +} diff --git a/src/Menus.h b/src/Menus.h new file mode 100644 index 0000000..9e2cd65 --- /dev/null +++ b/src/Menus.h @@ -0,0 +1,69 @@ +#ifndef CC_MENUS_H +#define CC_MENUS_H +#include "Gui.h" + +/* Contains all 2D menu screen implementations. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Screen; +struct MenuInputDesc; +struct FontDesc; +struct ButtonWidget; + +int Menu_InputDown(void* screen, int key); +int Menu_PointerDown(void* screen, int id, int x, int y); +int Menu_PointerMove(void* screen, int id, int x, int y); + +struct SimpleButtonDesc { short x, y; const char* title; Widget_LeftClick onClick; }; +void Menu_AddButtons(void* screen, struct ButtonWidget* btns, int width, + const struct SimpleButtonDesc* descs, int count); +void Menu_LayoutButtons(struct ButtonWidget* btns, + const struct SimpleButtonDesc* descs, int count); +void Menu_SetButtons(struct ButtonWidget* btns, struct FontDesc* font, + const struct SimpleButtonDesc* descs, int count); +void Menu_LayoutBack(struct ButtonWidget* btn); + +void PauseScreen_Show(void); +void OptionsGroupScreen_Show(void); +void ClassicOptionsScreen_Show(void); +void ClassicPauseScreen_Show(void); + +void ClassicBindingsScreen_Show(void); +void ClassicHacksBindingsScreen_Show(void); +void NormalBindingsScreen_Show(void); +void HacksBindingsScreen_Show(void); +void OtherBindingsScreen_Show(void); +void MouseBindingsScreen_Show(void); +void HotbarBindingsScreen_Show(void); + +void GenLevelScreen_Show(void); +void ClassicGenScreen_Show(void); +void LoadLevelScreen_Show(void); +void SaveLevelScreen_Show(void); +void TexturePackScreen_Show(void); +void FontListScreen_Show(void); +void HotkeyListScreen_Show(void); + +void MiscOptionsScreen_Show(void); +void ChatOptionsScreen_Show(void); +void GuiOptionsScreen_Show(void); +void GraphicsOptionsScreen_Show(void); +void HacksSettingsScreen_Show(void); +void EnvSettingsScreen_Show(void); +void NostalgiaAppearanceScreen_Show(void); +void NostalgiaFunctionalityScreen_Show(void); +void NostalgiaMenuScreen_Show(void); + +void UrlWarningOverlay_Show(const cc_string* url); +void TexIdsOverlay_Show(void); +void TexPackOverlay_Show(const cc_string* url); +#ifdef CC_BUILD_TOUCH +void TouchCtrlsScreen_Show(void); +void TouchMoreScreen_Show(void); +void TouchOnscreenScreen_Show(void); +#endif + +void MenuScreen_Render2(void* screen, float delta); +typedef void (*MenuInputDone)(const cc_string* value, cc_bool valid); +void MenuInputOverlay_Show(struct MenuInputDesc* desc, const cc_string* value, MenuInputDone onDone, cc_bool screenMode); +#endif diff --git a/src/Model.c b/src/Model.c new file mode 100644 index 0000000..ea02890 --- /dev/null +++ b/src/Model.c @@ -0,0 +1,2439 @@ +#include "Model.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Game.h" +#include "Graphics.h" +#include "Entity.h" +#include "Camera.h" +#include "Event.h" +#include "ExtMath.h" +#include "TexturePack.h" +#include "Drawer.h" +#include "Block.h" +#include "Stream.h" +#include "Options.h" + +struct _ModelsData Models; +/* NOTE: None of the built in models use more than 12 parts at once, but custom models can use up to 64 parts. */ +#define MODELS_MAX_VERTICES (MODEL_BOX_VERTICES * MAX_CUSTOM_MODEL_PARTS) + +#define UV_POS_MASK ((cc_uint16)0x7FFF) +#define UV_MAX ((cc_uint16)0x8000) +#define UV_MAX_SHIFT 15 +#define AABB_Width(bb) ((bb)->Max.x - (bb)->Min.x) +#define AABB_Height(bb) ((bb)->Max.y - (bb)->Min.y) +#define AABB_Length(bb) ((bb)->Max.z - (bb)->Min.z) + + +/*########################################################################################################################* +*------------------------------------------------------------Model--------------------------------------------------------* +*#########################################################################################################################*/ +static void Model_GetTransform(struct Entity* e, Vec3 pos, struct Matrix* m) { + Entity_GetTransform(e, pos, e->ModelScale, m); +} +static void Model_NullFunc(struct Entity* e) { } +static void Model_NoParts(void) { } + +void Model_Init(struct Model* model) { + model->bobbing = true; + model->usesSkin = true; + model->calcHumanAnims = false; + model->usesHumanSkin = false; + model->pushes = true; + + model->gravity = 0.08f; + Vec3_Set(model->drag, 0.91f, 0.98f, 0.91f); + Vec3_Set(model->groundFriction, 0.6f, 1.0f, 0.6f); + + model->maxScale = 2.0f; + model->shadowScale = 1.0f; + model->armX = 6; model->armY = 12; + + model->GetTransform = Model_GetTransform; + model->DrawArm = Model_NullFunc; +} + +cc_bool Model_ShouldRender(struct Entity* e) { + Vec3 pos = e->Position; + struct AABB bb; + float bbWidth, bbHeight, bbLength; + float maxYZ, maxXYZ; + + Entity_GetPickingBounds(e, &bb); + bbWidth = AABB_Width(&bb); + bbHeight = AABB_Height(&bb); + bbLength = AABB_Length(&bb); + + maxYZ = max(bbHeight, bbLength); + maxXYZ = max(bbWidth, maxYZ); + pos.y += bbHeight * 0.5f; /* Centre Y coordinate. */ + return FrustumCulling_SphereInFrustum(pos.x, pos.y, pos.z, maxXYZ); +} + +static float Model_MinDist(float dist, float extent) { + /* Compare min coord, centre coord, and max coord */ + float dMin = Math_AbsF(dist - extent), dMax = Math_AbsF(dist + extent); + float dMinMax = min(dMin, dMax); + return min(Math_AbsF(dist), dMinMax); +} + +float Model_RenderDistance(struct Entity* e) { + Vec3 pos = e->Position; + struct AABB* bb = &e->ModelAABB; + Vec3 camPos = Camera.CurrentPos; + float dx, dy, dz; + + /* X and Z are already at centre of model */ + /* Y is at feet, so needs to be moved up to centre */ + pos.y += AABB_Height(bb) * 0.5f; + + dx = Model_MinDist(camPos.x - pos.x, AABB_Width(bb) * 0.5f); + dy = Model_MinDist(camPos.y - pos.y, AABB_Height(bb) * 0.5f); + dz = Model_MinDist(camPos.z - pos.z, AABB_Length(bb) * 0.5f); + return dx * dx + dy * dy + dz * dz; +} + +void Model_Render(struct Model* model, struct Entity* e) { + struct Matrix m; + Vec3 pos = e->Position; + if (model->bobbing) pos.y += e->Anim.BobbingModel; + /* Original classic offsets models slightly into ground */ + if (Game_ClassicMode && (e->Flags & ENTITY_FLAG_CLASSIC_ADJUST)) + pos.y -= 1.5f / 16.0f; + + Model_SetupState(model, e); + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + + model->GetTransform(e, pos, &e->Transform); + Matrix_Mul(&m, &e->Transform, &Gfx.View); + + Gfx_LoadMatrix(MATRIX_VIEW, &m); + model->Draw(e); + Gfx_LoadMatrix(MATRIX_VIEW, &Gfx.View); +} + +void Model_SetupState(struct Model* model, struct Entity* e) { + PackedCol col; + float yawDelta; + + model->index = 0; + col = e->VTABLE->GetCol(e); + Models.Cols[0] = col; + + /* If a model forgets to call Model_ApplyTexture but still tries to draw, */ + /* then it is not using the model API properly. */ + /* So set uScale/vScale to ridiculous defaults to make it obvious */ + /* TODO: Remove setting this eventually */ + Models.uScale = 100.0f; + Models.vScale = 100.0f; + + if (!e->NoShade) { + Models.Cols[1] = PackedCol_Scale(col, PACKEDCOL_SHADE_YMIN); + Models.Cols[2] = PackedCol_Scale(col, PACKEDCOL_SHADE_Z); + Models.Cols[4] = PackedCol_Scale(col, PACKEDCOL_SHADE_X); + } else { + Models.Cols[1] = col; Models.Cols[2] = col; Models.Cols[4] = col; + } + + Models.Cols[3] = Models.Cols[2]; + Models.Cols[5] = Models.Cols[4]; + yawDelta = e->Yaw - e->RotY; + + Models.cosHead = Math_CosF(yawDelta * MATH_DEG2RAD); + Models.sinHead = Math_SinF(yawDelta * MATH_DEG2RAD); + Models.Active = model; +} + +void Model_ApplyTexture(struct Entity* e) { + struct Model* model = Models.Active; + struct ModelTex* data; + GfxResourceID tex; + cc_bool _64x64; + + tex = model->usesHumanSkin ? e->TextureId : e->MobTextureId; + if (tex) { + Models.skinType = e->SkinType; + } else { + data = model->defaultTex; + tex = data->texID; + Models.skinType = data->skinType; + } + + Gfx_BindTexture(tex); + _64x64 = Models.skinType != SKIN_64x32; + + Models.uScale = e->uScale * 0.015625f; + Models.vScale = e->vScale * (_64x64 ? 0.015625f : 0.03125f); +} + + +void Model_UpdateVB(void) { + struct Model* model = Models.Active; + if (!Models.Vb) + Models.Vb = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, Models.MaxVertices); + + Gfx_SetDynamicVbData(Models.Vb, Models.Vertices, model->index); + Gfx_DrawVb_IndexedTris(model->index); + model->index = 0; +} + +/* Need to restore vertices array to keep third party plugins such as MoreModels working */ +static struct VertexTextured* real_vertices; +static GfxResourceID modelVB; + +void Model_LockVB(struct Entity* entity, int verticesCount) { +#ifdef CC_BUILD_CONSOLE + if (!entity->ModelVB) { + entity->ModelVB = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, Models.Active->maxVertices); + } + modelVB = entity->ModelVB; +#else + if (!Models.Vb) { + Models.Vb = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, Models.MaxVertices); + } + modelVB = Models.Vb; +#endif + + real_vertices = Models.Vertices; + Models.Vertices = Gfx_LockDynamicVb(modelVB, VERTEX_FORMAT_TEXTURED, verticesCount); +} + +void Model_UnlockVB(void) { + Gfx_UnlockDynamicVb(modelVB); + Models.Vertices = real_vertices; +} + + +void Model_DrawPart(struct ModelPart* part) { + struct Model* model = Models.Active; + struct ModelVertex* src = &model->vertices[part->offset]; + struct VertexTextured* dst = &Models.Vertices[model->index]; + + struct ModelVertex v; + int i, count = part->count; + + for (i = 0; i < count; i++) { + v = *src; + dst->x = v.x; dst->y = v.y; dst->z = v.z; + dst->Col = Models.Cols[i >> 2]; + + dst->U = (v.u & UV_POS_MASK) * Models.uScale - (v.u >> UV_MAX_SHIFT) * 0.01f * Models.uScale; + dst->V = (v.v & UV_POS_MASK) * Models.vScale - (v.v >> UV_MAX_SHIFT) * 0.01f * Models.vScale; + src++; dst++; + } + model->index += count; +} + +#define Model_RotateX t = cosX * v.y + sinX * v.z; v.z = -sinX * v.y + cosX * v.z; v.y = t; +#define Model_RotateY t = cosY * v.x - sinY * v.z; v.z = sinY * v.x + cosY * v.z; v.x = t; +#define Model_RotateZ t = cosZ * v.x + sinZ * v.y; v.y = -sinZ * v.x + cosZ * v.y; v.x = t; + +void Model_DrawRotate(float angleX, float angleY, float angleZ, struct ModelPart* part, cc_bool head) { + struct Model* model = Models.Active; + struct ModelVertex* src = &model->vertices[part->offset]; + struct VertexTextured* dst = &Models.Vertices[model->index]; + + float cosX = Math_CosF(-angleX), sinX = Math_SinF(-angleX); + float cosY = Math_CosF(-angleY), sinY = Math_SinF(-angleY); + float cosZ = Math_CosF(-angleZ), sinZ = Math_SinF(-angleZ); + float t, x = part->rotX, y = part->rotY, z = part->rotZ; + + struct ModelVertex v; + int i, count = part->count; + + for (i = 0; i < count; i++) { + v = *src; + v.x -= x; v.y -= y; v.z -= z; + + /* Rotate locally */ + if (Models.Rotation == ROTATE_ORDER_ZYX) { + Model_RotateZ + Model_RotateY + Model_RotateX + } else if (Models.Rotation == ROTATE_ORDER_XZY) { + Model_RotateX + Model_RotateZ + Model_RotateY + } else if (Models.Rotation == ROTATE_ORDER_YZX) { + Model_RotateY + Model_RotateZ + Model_RotateX + } else if (Models.Rotation == ROTATE_ORDER_XYZ) { + Model_RotateX + Model_RotateY + Model_RotateZ + } + + /* Rotate globally (inlined RotY) */ + if (head) { + t = Models.cosHead * v.x - Models.sinHead * v.z; v.z = Models.sinHead * v.x + Models.cosHead * v.z; v.x = t; + } + dst->x = v.x + x; dst->y = v.y + y; dst->z = v.z + z; + dst->Col = Models.Cols[i >> 2]; + + dst->U = (v.u & UV_POS_MASK) * Models.uScale - (v.u >> UV_MAX_SHIFT) * 0.01f * Models.uScale; + dst->V = (v.v & UV_POS_MASK) * Models.vScale - (v.v >> UV_MAX_SHIFT) * 0.01f * Models.vScale; + src++; dst++; + } + model->index += count; +} + +void Model_RenderArm(struct Model* model, struct Entity* e) { + struct Matrix m, translate; + Vec3 pos = e->Position; + if (model->bobbing) pos.y += e->Anim.BobbingModel; + + Model_SetupState(model, e); + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Model_ApplyTexture(e); + + if (Models.ClassicArms) { + /* TODO: Position's not quite right. */ + /* Matrix_Translate(out m, -armX / 16f + 0.2f, -armY / 16f - 0.20f, 0); */ + /* is better, but that breaks the dig animation */ + Matrix_Translate(&translate, -model->armX / 16.0f, -model->armY / 16.0f - 0.10f, 0); + } else { + Matrix_Translate(&translate, -model->armX / 16.0f + 0.10f, -model->armY / 16.0f - 0.26f, 0); + } + + Entity_GetTransform(e, pos, e->ModelScale, &m); + Matrix_Mul(&m, &m, &Gfx.View); + Matrix_Mul(&m, &translate, &m); + + Gfx_LoadMatrix(MATRIX_VIEW, &m); + Models.Rotation = ROTATE_ORDER_YZX; + model->DrawArm(e); + Models.Rotation = ROTATE_ORDER_ZYX; + Gfx_LoadMatrix(MATRIX_VIEW, &Gfx.View); +} + +void Model_DrawArmPart(struct ModelPart* part) { + struct Model* model = Models.Active; + struct ModelPart arm = *part; + arm.rotX = model->armX / 16.0f; + arm.rotY = (model->armY + model->armY / 2) / 16.0f; + + if (Models.ClassicArms) { + Model_DrawRotate(0, -90 * MATH_DEG2RAD, 120 * MATH_DEG2RAD, &arm, false); + } else { + Model_DrawRotate(-20 * MATH_DEG2RAD, -70 * MATH_DEG2RAD, 135 * MATH_DEG2RAD, &arm, false); + } +} + + +/*########################################################################################################################* +*----------------------------------------------------------BoxDesc--------------------------------------------------------* +*#########################################################################################################################*/ +void BoxDesc_BuildBox(struct ModelPart* part, const struct BoxDesc* desc) { + int sidesW = desc->sizeZ, bodyW = desc->sizeX, bodyH = desc->sizeY; + float x1 = desc->x1, y1 = desc->y1, z1 = desc->z1; + float x2 = desc->x2, y2 = desc->y2, z2 = desc->z2; + int x = desc->texX, y = desc->texY; + struct Model* m = Models.Active; + + BoxDesc_YQuad2(m, x1, x2, z2, z1, y2, /* top */ + x + sidesW + bodyW, y, + x + sidesW, y + sidesW); + BoxDesc_YQuad2(m, x2, x1, z2, z1, y1, /* bottom */ + x + sidesW + bodyW, y, + x + sidesW + bodyW + bodyW, y + sidesW); + BoxDesc_ZQuad2(m, x1, x2, y1, y2, z1, /* front */ + x + sidesW + bodyW, y + sidesW, + x + sidesW, y + sidesW + bodyH); + BoxDesc_ZQuad2(m, x2, x1, y1, y2, z2, /* back */ + x + sidesW + bodyW + sidesW + bodyW, y + sidesW, + x + sidesW + bodyW + sidesW, y + sidesW + bodyH); + BoxDesc_XQuad2(m, z1, z2, y1, y2, x2, /* left */ + x + sidesW, y + sidesW, + x, y + sidesW + bodyH); + BoxDesc_XQuad2(m, z2, z1, y1, y2, x1, /* right */ + x + sidesW + bodyW + sidesW, y + sidesW, + x + sidesW + bodyW, y + sidesW + bodyH); + + ModelPart_Init(part, m->index - MODEL_BOX_VERTICES, MODEL_BOX_VERTICES, + desc->rotX, desc->rotY, desc->rotZ); +} + +void BoxDesc_BuildRotatedBox(struct ModelPart* part, const struct BoxDesc* desc) { + int sidesW = desc->sizeY, bodyW = desc->sizeX, bodyH = desc->sizeZ; + float x1 = desc->x1, y1 = desc->y1, z1 = desc->z1; + float x2 = desc->x2, y2 = desc->y2, z2 = desc->z2; + int x = desc->texX, y = desc->texY, i; + struct Model* m = Models.Active; + + BoxDesc_YQuad2(m, x1, x2, z1, z2, y2, /* top */ + x + sidesW + bodyW + sidesW, y + sidesW, + x + sidesW + bodyW + sidesW + bodyW, y + sidesW + bodyH); + BoxDesc_YQuad2(m, x2, x1, z1, z2, y1, /* bottom */ + x + sidesW, y + sidesW, + x + sidesW + bodyW, y + sidesW + bodyH); + BoxDesc_ZQuad2(m, x2, x1, y1, y2, z1, /* front */ + x + sidesW, y, + x + sidesW + bodyW, y + sidesW); + BoxDesc_ZQuad2(m, x1, x2, y2, y1, z2, /* back */ + x + sidesW + bodyW, y, + x + sidesW + bodyW + bodyW, y + sidesW); + BoxDesc_XQuad2(m, y2, y1, z2, z1, x2, /* left */ + x, y + sidesW, + x + sidesW, y + sidesW + bodyH); + BoxDesc_XQuad2(m, y1, y2, z2, z1, x1, /* right */ + x + sidesW + bodyW, y + sidesW, + x + sidesW + bodyW + sidesW, y + sidesW + bodyH); + + /* rotate left and right 90 degrees */ + for (i = m->index - 8; i < m->index; i++) { + struct ModelVertex vertex = m->vertices[i]; + float z = vertex.z; vertex.z = vertex.y; vertex.y = z; + m->vertices[i] = vertex; + } + + ModelPart_Init(part, m->index - MODEL_BOX_VERTICES, MODEL_BOX_VERTICES, + desc->rotX, desc->rotY, desc->rotZ); +} + + +void BoxDesc_XQuad(struct Model* m, int texX, int texY, int texWidth, int texHeight, float z1, float z2, float y1, float y2, float x, cc_bool swapU) { + int u1 = texX, u2 = texX + texWidth, tmp; + if (swapU) { tmp = u1; u1 = u2; u2 = tmp; } + BoxDesc_XQuad2(m, z1, z2, y1, y2, x, u1, texY, u2, texY + texHeight); +} + +void BoxDesc_YQuad(struct Model* m, int texX, int texY, int texWidth, int texHeight, float x1, float x2, float z1, float z2, float y, cc_bool swapU) { + int u1 = texX, u2 = texX + texWidth, tmp; + if (swapU) { tmp = u1; u1 = u2; u2 = tmp; } + BoxDesc_YQuad2(m, x1, x2, z1, z2, y, u1, texY, u2, texY + texHeight); +} + +void BoxDesc_ZQuad(struct Model* m, int texX, int texY, int texWidth, int texHeight, float x1, float x2, float y1, float y2, float z, cc_bool swapU) { + int u1 = texX, u2 = texX + texWidth, tmp; + if (swapU) { tmp = u1; u1 = u2; u2 = tmp; } + BoxDesc_ZQuad2(m, x1, x2, y1, y2, z, u1, texY, u2, texY + texHeight); +} + +void BoxDesc_XQuad2(struct Model* m, float z1, float z2, float y1, float y2, float x, int u1, int v1, int u2, int v2) { + if (u1 <= u2) { u2 |= UV_MAX; } else { u1 |= UV_MAX; } + if (v1 <= v2) { v2 |= UV_MAX; } else { v1 |= UV_MAX; } + + ModelVertex_Init(&m->vertices[m->index], x, y1, z1, u1, v2); m->index++; + ModelVertex_Init(&m->vertices[m->index], x, y2, z1, u1, v1); m->index++; + ModelVertex_Init(&m->vertices[m->index], x, y2, z2, u2, v1); m->index++; + ModelVertex_Init(&m->vertices[m->index], x, y1, z2, u2, v2); m->index++; +} + +void BoxDesc_YQuad2(struct Model* m, float x1, float x2, float z1, float z2, float y, int u1, int v1, int u2, int v2) { + if (u1 <= u2) { u2 |= UV_MAX; } else { u1 |= UV_MAX; } + if (v1 <= v2) { v2 |= UV_MAX; } else { v1 |= UV_MAX; } + + ModelVertex_Init(&m->vertices[m->index], x1, y, z2, u1, v2); m->index++; + ModelVertex_Init(&m->vertices[m->index], x1, y, z1, u1, v1); m->index++; + ModelVertex_Init(&m->vertices[m->index], x2, y, z1, u2, v1); m->index++; + ModelVertex_Init(&m->vertices[m->index], x2, y, z2, u2, v2); m->index++; +} + +void BoxDesc_ZQuad2(struct Model* m, float x1, float x2, float y1, float y2, float z, int u1, int v1, int u2, int v2) { + if (u1 <= u2) { u2 |= UV_MAX; } else { u1 |= UV_MAX; } + if (v1 <= v2) { v2 |= UV_MAX; } else { v1 |= UV_MAX; } + + ModelVertex_Init(&m->vertices[m->index], x1, y1, z, u1, v2); m->index++; + ModelVertex_Init(&m->vertices[m->index], x1, y2, z, u1, v1); m->index++; + ModelVertex_Init(&m->vertices[m->index], x2, y2, z, u2, v1); m->index++; + ModelVertex_Init(&m->vertices[m->index], x2, y1, z, u2, v2); m->index++; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Models common----------------------------------------------------* +*#########################################################################################################################*/ +static struct Model* models_head; +static struct Model* models_tail; +static struct ModelTex* textures_head; +static struct ModelTex* textures_tail; + +#define Model_RetSize(x,y,z) static Vec3 P = { (x)/16.0f,(y)/16.0f,(z)/16.0f }; e->Size = P; +#define Model_RetAABB(x1,y1,z1, x2,y2,z2) static struct AABB BB = { { (x1)/16.0f,(y1)/16.0f,(z1)/16.0f }, { (x2)/16.0f,(y2)/16.0f,(z2)/16.0f } }; e->ModelAABB = BB; + +static void MakeModel(struct Model* model) { + struct Model* active = Models.Active; + Models.Active = model; + model->MakeParts(); + + model->flags |= MODEL_FLAG_INITED; + model->index = 0; + Models.Active = active; +} + +struct Model* Model_Get(const cc_string* name) { + struct Model* model; + + for (model = models_head; model; model = model->next) { + if (!String_CaselessEqualsConst(name, model->name)) continue; + + if (!(model->flags & MODEL_FLAG_INITED)) + MakeModel(model); + return model; + } + return NULL; +} + +void Model_Register(struct Model* model) { + LinkedList_Append(model, models_head, models_tail); +} + +void Model_Unregister(struct Model* model) { + struct Model* cur; + int i; + LinkedList_Remove(model, cur, models_head, models_tail); + + /* unset this model from all entities, replacing with default fallback */ + for (i = 0; i < ENTITIES_MAX_COUNT; i++) + { + struct Entity* e = Entities.List[i]; + if (e && e->Model == model) { + cc_string humanModelName = String_FromReadonly(Models.Human->name); + Entity_SetModel(e, &humanModelName); + } + } +} + +void Model_RegisterTexture(struct ModelTex* tex) { + LinkedList_Append(tex, textures_head, textures_tail); +} + +static void Models_TextureChanged(void* obj, struct Stream* stream, const cc_string* name) { + struct ModelTex* tex; + + for (tex = textures_head; tex; tex = tex->next) + { + if (!String_CaselessEqualsConst(name, tex->name)) continue; + + Game_UpdateTexture(&tex->texID, stream, name, &tex->skinType, NULL); + return; + } +} + + +#ifdef CUSTOM_MODELS +/*########################################################################################################################* +*------------------------------------------------------Custom Models------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_LOWMEM +static struct CustomModel* custom_models; + +struct CustomModel* CustomModel_Get(int id) { + if (id >= MAX_CUSTOM_MODELS) return NULL; + + /* TODO log message if allocation fails? */ + if (!custom_models) + custom_models = Mem_TryAlloc(MAX_CUSTOM_MODELS, sizeof(struct CustomModel)); + + if (!custom_models) return NULL; + return &custom_models[id]; +} +#else +static struct CustomModel custom_models[MAX_CUSTOM_MODELS]; + +struct CustomModel* CustomModel_Get(int id) { + if (id >= MAX_CUSTOM_MODELS) return NULL; + + return &custom_models[id]; +} +#endif + +void CustomModel_BuildPart(struct CustomModel* cm, struct CustomModelPartDef* part) { + float x1 = part->min.x, y1 = part->min.y, z1 = part->min.z; + float x2 = part->max.x, y2 = part->max.y, z2 = part->max.z; + struct CustomModelPart* p = &cm->parts[cm->curPartIndex]; + + cm->model.index = cm->curPartIndex * MODEL_BOX_VERTICES; + p->fullbright = part->flags & 0x01; + p->firstPersonArm = part->flags & 0x02; + if (p->firstPersonArm) cm->numArmParts++; + + BoxDesc_YQuad2(&cm->model, x1, x2, z2, z1, y2, /* top */ + part->u1[0], part->v1[0], + part->u2[0], part->v2[0]); + BoxDesc_YQuad2(&cm->model, x2, x1, z2, z1, y1, /* bottom */ + part->u1[1], part->v1[1], + part->u2[1], part->v2[1]); + BoxDesc_ZQuad2(&cm->model, x1, x2, y1, y2, z1, /* front */ + part->u1[2], part->v1[2], + part->u2[2], part->v2[2]); + BoxDesc_ZQuad2(&cm->model, x2, x1, y1, y2, z2, /* back */ + part->u1[3], part->v1[3], + part->u2[3], part->v2[3]); + BoxDesc_XQuad2(&cm->model, z1, z2, y1, y2, x2, /* left */ + part->u1[4], part->v1[4], + part->u2[4], part->v2[4]); + BoxDesc_XQuad2(&cm->model, z2, z1, y1, y2, x1, /* right */ + part->u1[5], part->v1[5], + part->u2[5], part->v2[5]); + + ModelPart_Init(&p->modelPart, cm->model.index - MODEL_BOX_VERTICES, MODEL_BOX_VERTICES, + part->rotationOrigin.x, part->rotationOrigin.y, part->rotationOrigin.z); +} + +/* fmodf behaves differently on negatives vs positives, + but we want the function to be consistent on both so that the user can + specify direction using the "a" parameter in "Game.Time * anim->a". +*/ +static float EuclidianMod(float x, float y) { + return x - Math_Floor(x / y) * y; +} + +static struct ModelVertex oldVertices[MODEL_BOX_VERTICES]; +static float CustomModel_GetAnimationValue( + struct CustomModelAnim* anim, + struct CustomModelPart* part, + struct CustomModel* cm, + struct Entity* e +) { + switch (anim->type) { + case CustomModelAnimType_Head: + return -e->Pitch * MATH_DEG2RAD; + + case CustomModelAnimType_LeftLegX: + return e->Anim.LeftLegX; + + case CustomModelAnimType_RightLegX: + return e->Anim.RightLegX; + + case CustomModelAnimType_LeftArmX: + /* TODO: we're using 2 different rotation orders here */ + Models.Rotation = ROTATE_ORDER_XZY; + return e->Anim.LeftArmX; + + case CustomModelAnimType_LeftArmZ: + Models.Rotation = ROTATE_ORDER_XZY; + return e->Anim.LeftArmZ; + + case CustomModelAnimType_RightArmX: + Models.Rotation = ROTATE_ORDER_XZY; + return e->Anim.RightArmX; + + case CustomModelAnimType_RightArmZ: + Models.Rotation = ROTATE_ORDER_XZY; + return e->Anim.RightArmZ; + + /* + a: speed + b: shift pos + */ + case CustomModelAnimType_Spin: + return (float)Game.Time * anim->a + anim->b; + + case CustomModelAnimType_SpinVelocity: + return e->Anim.WalkTime * anim->a + anim->b; + + /* + a: speed + b: width + c: shift cycle + d: shift pos + */ + case CustomModelAnimType_SinRotate: + case CustomModelAnimType_SinTranslate: + case CustomModelAnimType_SinSize: + return ( Math_SinF((float)Game.Time * anim->a + 2 * MATH_PI * anim->c) + anim->d ) * anim->b; + + case CustomModelAnimType_SinRotateVelocity: + case CustomModelAnimType_SinTranslateVelocity: + case CustomModelAnimType_SinSizeVelocity: + return ( Math_SinF(e->Anim.WalkTime * anim->a + 2 * MATH_PI * anim->c) + anim->d ) * anim->b; + + /* + a: speed + b: width + c: shift cycle + d: max value + */ + case CustomModelAnimType_FlipRotate: + case CustomModelAnimType_FlipTranslate: + case CustomModelAnimType_FlipSize: + return EuclidianMod((float)Game.Time * anim->a + anim->c, anim->d) * anim->b; + + case CustomModelAnimType_FlipRotateVelocity: + case CustomModelAnimType_FlipTranslateVelocity: + case CustomModelAnimType_FlipSizeVelocity: + return EuclidianMod(e->Anim.WalkTime * anim->a + anim->c, anim->d) * anim->b; + } + + return 0.0f; +} + +static PackedCol oldCols[FACE_COUNT]; +static void CustomModel_DrawPart( + struct CustomModelPart* part, + struct CustomModel* cm, + struct Entity* e +) { + int i, animIndex; + float rotX, rotY, rotZ; + cc_bool head = false; + cc_bool modifiedVertices = false; + float value = 0.0f; + + if (part->fullbright) { + for (i = 0; i < FACE_COUNT; i++) + { + oldCols[i] = Models.Cols[i]; + Models.Cols[i] = PACKEDCOL_WHITE; + } + } + + /* bbmodels use xyz rotation order */ + Models.Rotation = ROTATE_ORDER_XYZ; + + rotX = part->rotation.x * MATH_DEG2RAD; + rotY = part->rotation.y * MATH_DEG2RAD; + rotZ = part->rotation.z * MATH_DEG2RAD; + + for (animIndex = 0; animIndex < MAX_CUSTOM_MODEL_ANIMS; animIndex++) + { + struct CustomModelAnim* anim = &part->anims[animIndex]; + if (anim->type == CustomModelAnimType_None) continue; + + value = CustomModel_GetAnimationValue(anim, part, cm, e); + + if ( + !modifiedVertices && + (anim->type == CustomModelAnimType_SinTranslate || + anim->type == CustomModelAnimType_SinTranslateVelocity || + anim->type == CustomModelAnimType_SinSize || + anim->type == CustomModelAnimType_SinSizeVelocity || + anim->type == CustomModelAnimType_FlipTranslate || + anim->type == CustomModelAnimType_FlipTranslateVelocity || + anim->type == CustomModelAnimType_FlipSize || + anim->type == CustomModelAnimType_FlipSizeVelocity) + ) { + modifiedVertices = true; + Mem_Copy( + oldVertices, + &cm->model.vertices[part->modelPart.offset], + sizeof(struct ModelVertex) * MODEL_BOX_VERTICES + ); + } + + if ( + anim->type == CustomModelAnimType_SinTranslate || + anim->type == CustomModelAnimType_SinTranslateVelocity || + anim->type == CustomModelAnimType_FlipTranslate || + anim->type == CustomModelAnimType_FlipTranslateVelocity + ) { + for (i = 0; i < MODEL_BOX_VERTICES; i++) { + struct ModelVertex* vertex = &cm->model.vertices[part->modelPart.offset + i]; + switch (anim->axis) { + case CustomModelAnimAxis_X: + vertex->x += value; + break; + + case CustomModelAnimAxis_Y: + vertex->y += value; + break; + + case CustomModelAnimAxis_Z: + vertex->z += value; + break; + } + } + } else if ( + anim->type == CustomModelAnimType_SinSize || + anim->type == CustomModelAnimType_SinSizeVelocity || + anim->type == CustomModelAnimType_FlipSize || + anim->type == CustomModelAnimType_FlipSizeVelocity + ) { + for (i = 0; i < MODEL_BOX_VERTICES; i++) { + struct ModelVertex* vertex = &cm->model.vertices[part->modelPart.offset + i]; + switch (anim->axis) { + case CustomModelAnimAxis_X: + vertex->x = Math_Lerp(part->modelPart.rotX, vertex->x, value); + break; + + case CustomModelAnimAxis_Y: + vertex->y = Math_Lerp(part->modelPart.rotY, vertex->y, value); + break; + + case CustomModelAnimAxis_Z: + vertex->z = Math_Lerp(part->modelPart.rotZ, vertex->z, value); + break; + } + } + } else { + if (anim->type == CustomModelAnimType_Head) { + head = true; + } + + switch (anim->axis) { + case CustomModelAnimAxis_X: + rotX += value; + break; + + case CustomModelAnimAxis_Y: + rotY += value; + break; + + case CustomModelAnimAxis_Z: + rotZ += value; + break; + } + } + } + + if (rotX || rotY || rotZ || head) { + Model_DrawRotate(rotX, rotY, rotZ, &part->modelPart, head); + } else { + Model_DrawPart(&part->modelPart); + } + + if (modifiedVertices) { + Mem_Copy( + &cm->model.vertices[part->modelPart.offset], + oldVertices, + sizeof(struct ModelVertex) * MODEL_BOX_VERTICES + ); + } + + if (part->fullbright) { + for (i = 0; i < FACE_COUNT; i++) + { + Models.Cols[i] = oldCols[i]; + } + } +} + +static void CustomModel_Draw(struct Entity* e) { + struct CustomModel* cm = (struct CustomModel*)Models.Active; + int i; + + Model_ApplyTexture(e); + Models.uScale = e->uScale / cm->uScale; + Models.vScale = e->vScale / cm->vScale; + Model_LockVB(e, cm->numParts * MODEL_BOX_VERTICES); + + for (i = 0; i < cm->numParts; i++) + { + CustomModel_DrawPart(&cm->parts[i], cm, e); + } + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(cm->numParts * MODEL_BOX_VERTICES); + Models.Rotation = ROTATE_ORDER_ZYX; +} + +static float CustomModel_GetNameY(struct Entity* e) { + return ((struct CustomModel*)e->Model)->nameY; +} + +static float CustomModel_GetEyeY(struct Entity* e) { + return ((struct CustomModel*)e->Model)->eyeY; +} + +static void CustomModel_GetCollisionSize(struct Entity* e) { + e->Size = ((struct CustomModel*)e->Model)->collisionBounds; +} + +static void CustomModel_GetPickingBounds(struct Entity* e) { + e->ModelAABB = ((struct CustomModel*)e->Model)->pickingBoundsAABB; +} + +static void CustomModel_DrawArm(struct Entity* e) { + struct CustomModel* cm = (struct CustomModel*)Models.Active; + int i; + if (!cm->numArmParts) return; + Gfx_SetAlphaTest(true); + + Models.uScale = 1.0f / cm->uScale; + Models.vScale = 1.0f / cm->vScale; + Model_LockVB(e, cm->numArmParts * MODEL_BOX_VERTICES); + + for (i = 0; i < cm->numParts; i++) + { + struct CustomModelPart* part = &cm->parts[i]; + if (part->firstPersonArm) { + Model_DrawArmPart(&part->modelPart); + } + } + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(cm->numArmParts * MODEL_BOX_VERTICES); +} + +static void CheckMaxVertices(void) { + /* hack to undo plugins setting a smaller vertices buffer */ + if (Models.MaxVertices < MODELS_MAX_VERTICES) { + Platform_LogConst("CheckMaxVertices found smaller buffer, resetting Models.Vb"); + Models.MaxVertices = MODELS_MAX_VERTICES; + Gfx_DeleteDynamicVb(&Models.Vb); + } +} + +void CustomModel_Register(struct CustomModel* cm) { + static struct ModelTex customDefaultTex; + + CheckMaxVertices(); + cm->model.name = cm->name; + cm->model.defaultTex = &customDefaultTex; + + cm->model.MakeParts = Model_NoParts; + cm->model.Draw = CustomModel_Draw; + cm->model.GetNameY = CustomModel_GetNameY; + cm->model.GetEyeY = CustomModel_GetEyeY; + cm->model.GetCollisionSize = CustomModel_GetCollisionSize; + cm->model.GetPickingBounds = CustomModel_GetPickingBounds; + cm->model.DrawArm = CustomModel_DrawArm; + + /* add to front of models linked list to override original models */ + if (!models_head) { + models_head = &cm->model; + models_tail = models_head; + } else { + cm->model.next = models_head; + models_head = &cm->model; + } + cm->registered = true; +} + +void CustomModel_Undefine(struct CustomModel* cm) { + if (!cm->defined) return; + if (cm->registered) Model_Unregister((struct Model*)cm); + + Mem_Free(cm->model.vertices); + Mem_Set(cm, 0, sizeof(struct CustomModel)); +} + +static void CustomModel_FreeAll(void) { + int i; + if (!custom_models) return; + + for (i = 0; i < MAX_CUSTOM_MODELS; i++) + { + CustomModel_Undefine(&custom_models[i]); + } +} +#else +static void CustomModel_FreeAll(void) { } +#endif + + +/*########################################################################################################################* +*---------------------------------------------------------HumanModel------------------------------------------------------* +*#########################################################################################################################*/ +struct ModelLimbs { + struct ModelPart leftLeg, rightLeg, leftArm, rightArm, leftLegLayer, rightLegLayer, leftArmLayer, rightArmLayer; +}; +struct ModelSet { + struct ModelPart head, torso, hat, torsoLayer; + struct ModelLimbs limbs[3]; +}; +#define HUMAN_BASE_VERTICES (6 * MODEL_BOX_VERTICES) +#define HUMAN_HAT32_VERTICES (1 * MODEL_BOX_VERTICES) +#define HUMAN_HAT64_VERTICES (6 * MODEL_BOX_VERTICES) +#define HUMAN_MAX_VERTICES HUMAN_BASE_VERTICES + HUMAN_HAT64_VERTICES + +static void HumanModel_DrawCore(struct Entity* e, struct ModelSet* model, cc_bool opaqueBody) { + struct ModelLimbs* set; + int type, num; + Model_ApplyTexture(e); + + type = Models.skinType; + set = &model->limbs[type & 0x3]; + num = HUMAN_BASE_VERTICES + (type == SKIN_64x32 ? HUMAN_HAT32_VERTICES : HUMAN_HAT64_VERTICES); + Model_LockVB(e, num); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &model->head, true); + Model_DrawPart(&model->torso); + Model_DrawRotate(e->Anim.LeftLegX, 0, e->Anim.LeftLegZ, &set->leftLeg, false); + Model_DrawRotate(e->Anim.RightLegX, 0, e->Anim.RightLegZ, &set->rightLeg, false); + + Models.Rotation = ROTATE_ORDER_XZY; + Model_DrawRotate(e->Anim.LeftArmX, 0, e->Anim.LeftArmZ, &set->leftArm, false); + Model_DrawRotate(e->Anim.RightArmX, 0, e->Anim.RightArmZ, &set->rightArm, false); + Models.Rotation = ROTATE_ORDER_ZYX; + + if (type != SKIN_64x32) { + Model_DrawPart(&model->torsoLayer); + Model_DrawRotate(e->Anim.LeftLegX, 0, e->Anim.LeftLegZ, &set->leftLegLayer, false); + Model_DrawRotate(e->Anim.RightLegX, 0, e->Anim.RightLegZ, &set->rightLegLayer, false); + + Models.Rotation = ROTATE_ORDER_XZY; + Model_DrawRotate(e->Anim.LeftArmX, 0, e->Anim.LeftArmZ, &set->leftArmLayer, false); + Model_DrawRotate(e->Anim.RightArmX, 0, e->Anim.RightArmZ, &set->rightArmLayer, false); + Models.Rotation = ROTATE_ORDER_ZYX; + } + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &model->hat, true); + + Model_UnlockVB(); + if (opaqueBody) { + /* human model draws the body opaque so players can't have invisible skins */ + Gfx_SetAlphaTest(false); + Gfx_DrawVb_IndexedTris_Range(HUMAN_BASE_VERTICES, 0); + Gfx_SetAlphaTest(true); + Gfx_DrawVb_IndexedTris_Range(num - HUMAN_BASE_VERTICES, HUMAN_BASE_VERTICES); + } else { + Gfx_DrawVb_IndexedTris(num); + } +} + +static void HumanModel_DrawArmCore(struct Entity* e, struct ModelSet* model) { + struct ModelLimbs* set; + int type, num; + Gfx_SetAlphaTest(true); + + type = Models.skinType; + set = &model->limbs[type & 0x3]; + num = type == SKIN_64x32 ? MODEL_BOX_VERTICES : (2 * MODEL_BOX_VERTICES); + Model_LockVB(e, num); + + Model_DrawArmPart(&set->rightArm); + if (type != SKIN_64x32) Model_DrawArmPart(&set->rightArmLayer); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(num); +} + + +static struct ModelSet human_set; +static void HumanModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-4,24,-4, 4,32,4), + BoxDesc_Rot(0,24,0), + }; + static const struct BoxDesc torso = { + BoxDesc_Tex(16,16), + BoxDesc_Box(-4,12,-2, 4,24,2), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc hat = { + BoxDesc_Tex(32,0), + BoxDesc_Dims(-4,24,-4, 4,32,4), + BoxDesc_Bounds(-4.5f,23.5f,-4.5f, 4.5f,32.5f,4.5f), + BoxDesc_Rot(0,24,0), + }; + static const struct BoxDesc torsoL = { + BoxDesc_Tex(16,32), + BoxDesc_Dims(-4,12,-2, 4,24,2), + BoxDesc_Bounds(-4.5f,11.5f,-2.5f, 4.5f,24.5f,2.5f), + BoxDesc_Rot(0,12,0), + }; + + static const struct BoxDesc lArm = { + BoxDesc_Tex(40,16), + BoxDesc_Box(-4,12,-2, -8,24,2), + BoxDesc_Rot(-5,22,0), + }; + static const struct BoxDesc rArm = { + BoxDesc_Tex(40,16), + BoxDesc_Box(4,12,-2, 8,24,2), + BoxDesc_Rot(5,22,0), + }; + static const struct BoxDesc lLeg = { + BoxDesc_Tex(0,16), + BoxDesc_Box(0,0,-2, -4,12,2), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc rLeg = { + BoxDesc_Tex(0,16), + BoxDesc_Box(0,0,-2, 4,12,2), + BoxDesc_Rot(0,12,0), + }; + + static const struct BoxDesc lArm64 = { + BoxDesc_Tex(32,48), + BoxDesc_Box(-8,12,-2, -4,24,2), + BoxDesc_Rot(-5,22,0), + }; + static const struct BoxDesc lLeg64 = { + BoxDesc_Tex(16,48), + BoxDesc_Box(-4,0,-2, 0,12,2), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc lArmL = { + BoxDesc_Tex(48,48), + BoxDesc_Dims(-8,12,-2, -4,24,2), + BoxDesc_Bounds(-8.5f,11.5f,-2.5f, -3.5f,24.5f,2.5f), + BoxDesc_Rot(-5,22,0), + }; + static const struct BoxDesc rArmL = { + BoxDesc_Tex(40,32), + BoxDesc_Dims(4,12,-2, 8,24,2), + BoxDesc_Bounds(3.5f,11.5f,-2.5f, 8.5f,24.5f,2.5f), + BoxDesc_Rot(5,22,0), + }; + static const struct BoxDesc lLegL = { + BoxDesc_Tex(0,48), + BoxDesc_Dims(-4,0,-2, 0,12,2), + BoxDesc_Bounds(-4.5f,-0.5f,-2.5f, 0.5f,12.5f,2.5f), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc rLegL = { + BoxDesc_Tex(0,32), + BoxDesc_Dims(0,0,-2, 4,12,2), + BoxDesc_Bounds(-0.5f,-0.5f,-2.5f, 4.5f,12.5f,2.5f), + BoxDesc_Rot(0,12,0), + }; + + /* Thin arms - make sure to keep in sync with lArm64/rArm/lArmL/rArmL */ + static const struct BoxDesc thin_lArm = { + BoxDesc_Tex(32,48), + BoxDesc_Box(-7,12,-2, -4,24,2), + BoxDesc_Rot(-5,22,0), + }; + static const struct BoxDesc thin_rArm = { + BoxDesc_Tex(40,16), + BoxDesc_Box(4,12,-2, 7,24,2), + BoxDesc_Rot(5,22,0), + }; + static const struct BoxDesc thin_lArmL = { + BoxDesc_Tex(48,48), + BoxDesc_Dims(-7,12,-2, -4,24,2), + BoxDesc_Bounds(-7.5f,11.5f,-2.5f, -3.5f,24.5f,2.5f), + BoxDesc_Rot(-5,22,0), + }; + static const struct BoxDesc thin_rArmL = { + BoxDesc_Tex(40,32), + BoxDesc_Dims(4,12,-2, 7,24,2), + BoxDesc_Bounds(3.5f,11.5f,-2.5f, 7.5f,24.5f,2.5f), + BoxDesc_Rot(5,22,0), + }; + + struct ModelLimbs* set = &human_set.limbs[0]; + struct ModelLimbs* set64 = &human_set.limbs[1]; + struct ModelLimbs* setSlim = &human_set.limbs[2]; + + BoxDesc_BuildBox(&human_set.head, &head); + BoxDesc_BuildBox(&human_set.torso, &torso); + BoxDesc_BuildBox(&human_set.hat, &hat); + BoxDesc_BuildBox(&human_set.torsoLayer, &torsoL); + + BoxDesc_BuildBox(&set->leftLeg, &lLeg); + BoxDesc_BuildBox(&set->rightLeg, &rLeg); + BoxDesc_BuildBox(&set->leftArm, &lArm); + BoxDesc_BuildBox(&set->rightArm, &rArm); + + /* 64x64 arms and legs */ + BoxDesc_BuildBox(&set64->leftLeg, &lLeg64); + set64->rightLeg = set->rightLeg; + BoxDesc_BuildBox(&set64->leftArm, &lArm64); + set64->rightArm = set->rightArm; + + /* Thin arms and legs */ + setSlim->leftLeg = set64->leftLeg; + setSlim->rightLeg = set64->rightLeg; + BoxDesc_BuildBox(&setSlim->leftArm, &thin_lArm); + BoxDesc_BuildBox(&setSlim->rightArm, &thin_rArm); + + /* 64x64 arm and leg layers */ + BoxDesc_BuildBox(&set64->leftLegLayer, &lLegL); + BoxDesc_BuildBox(&set64->rightLegLayer, &rLegL); + BoxDesc_BuildBox(&set64->leftArmLayer, &lArmL); + BoxDesc_BuildBox(&set64->rightArmLayer, &rArmL); + + /* Thin arm and leg layers */ + setSlim->leftLegLayer = set64->leftLegLayer; + setSlim->rightLegLayer = set64->rightLegLayer; + BoxDesc_BuildBox(&setSlim->leftArmLayer, &thin_lArmL); + BoxDesc_BuildBox(&setSlim->rightArmLayer, &thin_rArmL); +} + +static void HumanModel_Draw(struct Entity* e) { + HumanModel_DrawCore(e, &human_set, true); +} + +static void HumanModel_DrawArm(struct Entity* e) { + HumanModel_DrawArmCore(e, &human_set); +} + +static float HumanModel_GetNameY(struct Entity* e) { return 32.5f/16.0f; } +static float HumanModel_GetEyeY(struct Entity* e) { return 26.0f/16.0f; } +static void HumanModel_GetSize(struct Entity* e) { Model_RetSize(8.6f,28.1f,8.6f); } +static void HumanModel_GetBounds(struct Entity* e) { Model_RetAABB(-8,0,-4, 8,32,4); } + +static struct ModelVertex human_vertices[MODEL_BOX_VERTICES * (7 + 7 + 4)]; +static struct ModelTex human_tex = { "char.png" }; +static struct Model human_model = { + "humanoid", human_vertices, &human_tex, + HumanModel_MakeParts, HumanModel_Draw, + HumanModel_GetNameY, HumanModel_GetEyeY, + HumanModel_GetSize, HumanModel_GetBounds, +}; + +static void HumanoidModel_Register(void) { + Model_Init(&human_model); + human_model.DrawArm = HumanModel_DrawArm; + + human_model.calcHumanAnims = true; + human_model.usesHumanSkin = true; + human_model.flags |= MODEL_FLAG_CLEAR_HAT; + human_model.maxVertices = HUMAN_MAX_VERTICES; + + Model_Register(&human_model); +} + + +/*########################################################################################################################* +*---------------------------------------------------------ChibiModel------------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelSet chibi_set; + +CC_NOINLINE static void ChibiModel_ScalePart(struct ModelPart* dst, struct ModelPart* src) { + struct Model* chibi = Models.Active; + int i; + + *dst = *src; + dst->rotX *= 0.5f; dst->rotY *= 0.5f; dst->rotZ *= 0.5f; + + for (i = src->offset; i < src->offset + src->count; i++) { + chibi->vertices[i] = human_model.vertices[i]; + + chibi->vertices[i].x *= 0.5f; + chibi->vertices[i].y *= 0.5f; + chibi->vertices[i].z *= 0.5f; + } +} + +CC_NOINLINE static void ChibiModel_ScaleLimbs(struct ModelLimbs* dst, struct ModelLimbs* src) { + ChibiModel_ScalePart(&dst->leftLeg, &src->leftLeg); + ChibiModel_ScalePart(&dst->rightLeg, &src->rightLeg); + ChibiModel_ScalePart(&dst->leftArm, &src->leftArm); + ChibiModel_ScalePart(&dst->rightArm, &src->rightArm); + + ChibiModel_ScalePart(&dst->leftLegLayer, &src->leftLegLayer); + ChibiModel_ScalePart(&dst->rightLegLayer, &src->rightLegLayer); + ChibiModel_ScalePart(&dst->leftArmLayer, &src->leftArmLayer); + ChibiModel_ScalePart(&dst->rightArmLayer, &src->rightArmLayer); +} + +static void ChibiModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-4,12,-4, 4,20,4), + BoxDesc_Rot(0,13,0), + }; + static const struct BoxDesc hat = { + BoxDesc_Tex(32,0), + BoxDesc_Dims(-4,12,-4, 4,20,4), + BoxDesc_Bounds(-4.25f,11.75f,-4.25f, 4.25f,20.25f,4.25f), + BoxDesc_Rot(0,13,0), + }; + + /* Chibi is mostly just half scale humanoid */ + ChibiModel_ScalePart(&chibi_set.torso, &human_set.torso); + ChibiModel_ScalePart(&chibi_set.torsoLayer, &human_set.torsoLayer); + ChibiModel_ScaleLimbs(&chibi_set.limbs[0], &human_set.limbs[0]); + ChibiModel_ScaleLimbs(&chibi_set.limbs[1], &human_set.limbs[1]); + ChibiModel_ScaleLimbs(&chibi_set.limbs[2], &human_set.limbs[2]); + + /* But head is at normal size */ + Models.Active->index = human_set.head.offset; + BoxDesc_BuildBox(&chibi_set.head, &head); + Models.Active->index = human_set.hat.offset; + BoxDesc_BuildBox(&chibi_set.hat, &hat); +} + +static void ChibiModel_Draw(struct Entity* e) { + HumanModel_DrawCore(e, &chibi_set, true); +} + +static void ChibiModel_DrawArm(struct Entity* e) { + HumanModel_DrawArmCore(e, &chibi_set); +} + +static float ChibiModel_GetNameY(struct Entity* e) { return 20.2f/16.0f; } +static float ChibiModel_GetEyeY(struct Entity* e) { return 14.0f/16.0f; } +static void ChibiModel_GetSize(struct Entity* e) { Model_RetSize(4.6f,20.1f,4.6f); } +static void ChibiModel_GetBounds(struct Entity* e) { Model_RetAABB(-4,0,-4, 4,16,4); } + +static struct ModelVertex chibi_vertices[MODEL_BOX_VERTICES * (7 + 7 + 4)]; +static struct Model chibi_model = { "chibi", chibi_vertices, &human_tex, + ChibiModel_MakeParts, ChibiModel_Draw, + ChibiModel_GetNameY, ChibiModel_GetEyeY, + ChibiModel_GetSize, ChibiModel_GetBounds +}; + +static void ChibiModel_Register(void) { + Model_Init(&chibi_model); + chibi_model.DrawArm = ChibiModel_DrawArm; + chibi_model.armX = 3; + chibi_model.armY = 6; + + chibi_model.calcHumanAnims = true; + chibi_model.usesHumanSkin = true; + chibi_model.flags |= MODEL_FLAG_CLEAR_HAT; + chibi_model.maxVertices = HUMAN_MAX_VERTICES; + + chibi_model.maxScale = 3.0f; + chibi_model.shadowScale = 0.5f; + Model_Register(&chibi_model); +} + + +/*########################################################################################################################* +*--------------------------------------------------------SittingModel-----------------------------------------------------* +*#########################################################################################################################*/ +#define SIT_OFFSET 10.0f + +static void SittingModel_GetTransform(struct Entity* e, Vec3 pos, struct Matrix* m) { + pos.y -= (SIT_OFFSET / 16.0f) * e->ModelScale.y; + Entity_GetTransform(e, pos, e->ModelScale, m); +} + +static void SittingModel_Draw(struct Entity* e) { + e->Anim.LeftLegX = 1.5f; e->Anim.RightLegX = 1.5f; + e->Anim.LeftLegZ = -0.1f; e->Anim.RightLegZ = 0.1f; + HumanModel_Draw(e); +} + +static float SittingModel_GetEyeY(struct Entity* e) { return (26.0f - SIT_OFFSET) / 16.0f; } +static void SittingModel_GetSize(struct Entity* e) { Model_RetSize(8.6f,28.1f - SIT_OFFSET,8.6f); } +static void SittingModel_GetBounds(struct Entity* e) { Model_RetAABB(-8,0,-4, 8,32 - SIT_OFFSET,4); } + +static struct Model sitting_model = { "sit", human_vertices, &human_tex, + Model_NoParts, SittingModel_Draw, + HumanModel_GetNameY, SittingModel_GetEyeY, + SittingModel_GetSize, SittingModel_GetBounds +}; + +static void SittingModel_Register(void) { + Model_Init(&sitting_model); + sitting_model.DrawArm = HumanModel_DrawArm; + + sitting_model.calcHumanAnims = true; + sitting_model.usesHumanSkin = true; + sitting_model.flags |= MODEL_FLAG_CLEAR_HAT; + sitting_model.maxVertices = HUMAN_MAX_VERTICES; + + sitting_model.shadowScale = 0.5f; + sitting_model.GetTransform = SittingModel_GetTransform; + Model_Register(&sitting_model); +} + + +/*########################################################################################################################* +*--------------------------------------------------------CorpseModel------------------------------------------------------* +*#########################################################################################################################*/ +static void CorpseModel_Draw(struct Entity* e) { + e->Anim.LeftLegX = 0.025f; e->Anim.RightLegX = 0.025f; + e->Anim.LeftArmX = 0.025f; e->Anim.RightArmX = 0.025f; + e->Anim.LeftLegZ = -0.15f; e->Anim.RightLegZ = 0.15f; + e->Anim.LeftArmZ = -0.20f; e->Anim.RightArmZ = 0.20f; + HumanModel_Draw(e); +} + +static struct Model corpse_model; +static void CorpseModel_Register(void) { + corpse_model = human_model; + corpse_model.name = "corpse"; + + corpse_model.MakeParts = Model_NoParts; + corpse_model.Draw = CorpseModel_Draw; + Model_Register(&corpse_model); +} + + +/*########################################################################################################################* +*---------------------------------------------------------HeadModel-------------------------------------------------------* +*#########################################################################################################################*/ +#define HEAD_MAX_VERTICES (2 * MODEL_BOX_VERTICES) + +static void HeadModel_GetTransform(struct Entity* e, Vec3 pos, struct Matrix* m) { + pos.y -= (24.0f/16.0f) * e->ModelScale.y; + Entity_GetTransform(e, pos, e->ModelScale, m); +} + +static void HeadModel_Draw(struct Entity* e) { + struct ModelPart part; + Model_ApplyTexture(e); + Model_LockVB(e, HEAD_MAX_VERTICES); + + part = human_set.head; part.rotY += 4.0f/16.0f; + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &part, true); + part = human_set.hat; part.rotY += 4.0f/16.0f; + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &part, true); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(HEAD_MAX_VERTICES); +} + +static float HeadModel_GetEyeY(struct Entity* e) { return 6.0f/16.0f; } +static void HeadModel_GetSize(struct Entity* e) { Model_RetSize(7.9f,7.9f,7.9f); } +static void HeadModel_GetBounds(struct Entity* e) { Model_RetAABB(-4,0,-4, 4,8,4); } + +static struct Model head_model = { "head", human_vertices, &human_tex, + Model_NoParts, HeadModel_Draw, + HumanModel_GetNameY, HeadModel_GetEyeY, + HeadModel_GetSize, HeadModel_GetBounds +}; + +static void HeadModel_Register(void) { + Model_Init(&head_model); + head_model.usesHumanSkin = true; + head_model.flags |= MODEL_FLAG_CLEAR_HAT; + + head_model.pushes = false; + head_model.GetTransform = HeadModel_GetTransform; + head_model.maxVertices = HEAD_MAX_VERTICES; + Model_Register(&head_model); +} + + +/*########################################################################################################################* +*--------------------------------------------------------ChickenModel-----------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart chicken_head, chicken_wattle, chicken_beak, chicken_torso; +static struct ModelPart chicken_leftLeg, chicken_rightLeg, chicken_leftWing, Chicken_RightWing; +#define CHICKEN_MAX_VERTICES (6 * MODEL_BOX_VERTICES + 2 * (MODEL_QUAD_VERTICES * 2)) + +static void ChickenModel_MakeLeg(struct ModelPart* part, int x1, int x2, int legX1, int legX2) { +#define ch_y1 ( 1.0f/64.0f) +#define ch_y2 ( 5.0f/16.0f) +#define ch_z2 ( 1.0f/16.0f) +#define ch_z1 (-2.0f/16.0f) + + struct Model* m = Models.Active; + BoxDesc_YQuad2(m, x2/16.0f, x1/16.0f, ch_z1, ch_z2, ch_y1, + 32,0, 35,3); /* bottom feet */ + BoxDesc_ZQuad2(m, legX1/16.0f, legX2/16.0f, ch_y1, ch_y2, ch_z2, + 36,3, 37,8); /* vertical part of leg */ + + ModelPart_Init(part, m->index - MODEL_QUAD_VERTICES * 2, MODEL_QUAD_VERTICES * 2, + 0.0f/16.0f, 5.0f/16.0f, 1.0f/16.0f); +} + +static void ChickenModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-2,9,-6, 2,15,-3), + BoxDesc_Rot(0,9,-4), + }; + static const struct BoxDesc wattle = { + BoxDesc_Tex(14,4), + BoxDesc_Box(-1,9,-7, 1,11,-5), + BoxDesc_Rot(0,9,-4), + }; + static const struct BoxDesc beak = { + BoxDesc_Tex(14,0), + BoxDesc_Box(-2,11,-8, 2,13,-6), + BoxDesc_Rot(0,9,-4), + }; + static const struct BoxDesc torso = { + BoxDesc_Tex(0,9), + BoxDesc_Box(-3,5,-4, 3,11,3), + BoxDesc_Rot(0,5,0), + }; + static const struct BoxDesc lWing = { + BoxDesc_Tex(24,13), + BoxDesc_Box(-4,7,-3, -3,11,3), + BoxDesc_Rot(-3,11,0), + }; + static const struct BoxDesc rWing = { + BoxDesc_Tex(24,13), + BoxDesc_Box(3,7,-3, 4,11,3), + BoxDesc_Rot(3,11,0), + }; + + BoxDesc_BuildBox(&chicken_head, &head); + BoxDesc_BuildBox(&chicken_wattle, &wattle); + BoxDesc_BuildBox(&chicken_beak, &beak); + BoxDesc_BuildRotatedBox(&chicken_torso, &torso); + BoxDesc_BuildBox(&chicken_leftWing, &lWing); + BoxDesc_BuildBox(&Chicken_RightWing, &rWing); + + ChickenModel_MakeLeg(&chicken_leftLeg, -3, 0, -2, -1); + ChickenModel_MakeLeg(&chicken_rightLeg, 0, 3, 1, 2); +} + +static void ChickenModel_Draw(struct Entity* e) { + PackedCol col = Models.Cols[0]; + int i; + Model_ApplyTexture(e); + Model_LockVB(e, CHICKEN_MAX_VERTICES); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &chicken_head, true); + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &chicken_wattle, true); + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &chicken_beak, true); + + Model_DrawPart(&chicken_torso); + Model_DrawRotate(0, 0, -Math_AbsF(e->Anim.LeftArmX), &chicken_leftWing, false); + Model_DrawRotate(0, 0, Math_AbsF(e->Anim.LeftArmX), &Chicken_RightWing, false); + + for (i = 0; i < FACE_COUNT; i++) + { + Models.Cols[i] = PackedCol_Scale(col, 0.7f); + } + + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &chicken_leftLeg, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &chicken_rightLeg, false); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(CHICKEN_MAX_VERTICES); +} + +static float ChickenModel_GetNameY(struct Entity* e) { return 1.0125f; } +static float ChickenModel_GetEyeY(struct Entity* e) { return 14.0f/16.0f; } +static void ChickenModel_GetSize(struct Entity* e) { Model_RetSize(8.0f,12.0f,8.0f); } +static void ChickenModel_GetBounds(struct Entity* e) { Model_RetAABB(-4,0,-8, 4,15,4); } + +static struct ModelVertex chicken_vertices[MODEL_BOX_VERTICES * 6 + (MODEL_QUAD_VERTICES * 2) * 2]; +static struct ModelTex chicken_tex = { "chicken.png" }; +static struct Model chicken_model = { "chicken", chicken_vertices, &chicken_tex, + ChickenModel_MakeParts, ChickenModel_Draw, + ChickenModel_GetNameY, ChickenModel_GetEyeY, + ChickenModel_GetSize, ChickenModel_GetBounds +}; + +static void ChickenModel_Register(void) { + Model_Init(&chicken_model); + chicken_model.maxVertices = CHICKEN_MAX_VERTICES; + Model_Register(&chicken_model); +} + + +/*########################################################################################################################* +*--------------------------------------------------------CreeperModel-----------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart creeper_head, creeper_torso, creeper_leftLegFront; +static struct ModelPart creeper_rightLegFront, creeper_leftLegBack, creeper_rightLegBack; +#define CREEPER_MAX_VERTICES (6 * MODEL_BOX_VERTICES) + +static void CreeperModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-4,18,-4, 4,26,4), + BoxDesc_Rot(0,18,0), + }; + static const struct BoxDesc torso = { + BoxDesc_Tex(16,16), + BoxDesc_Box(-4,6,-2, 4,18,2), + BoxDesc_Rot(0,6,0), + }; + static const struct BoxDesc lFront = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-4,0,-6, 0,6,-2), + BoxDesc_Rot(0,6,-2), + }; + static const struct BoxDesc rFront = { + BoxDesc_Tex(0,16), + BoxDesc_Box(0,0,-6, 4,6,-2), + BoxDesc_Rot(0,6,-2), + }; + static const struct BoxDesc lBack = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-4,0,2, 0,6,6), + BoxDesc_Rot(0,6,2), + }; + static const struct BoxDesc rBack = { + BoxDesc_Tex(0,16), + BoxDesc_Box(0,0,2, 4,6,6), + BoxDesc_Rot(0,6,2), + }; + + BoxDesc_BuildBox(&creeper_head, &head); + BoxDesc_BuildBox(&creeper_torso, &torso); + BoxDesc_BuildBox(&creeper_leftLegFront, &lFront); + BoxDesc_BuildBox(&creeper_rightLegFront, &rFront); + BoxDesc_BuildBox(&creeper_leftLegBack, &lBack); + BoxDesc_BuildBox(&creeper_rightLegBack, &rBack); +} + +static void CreeperModel_Draw(struct Entity* e) { + Model_ApplyTexture(e); + Model_LockVB(e, CREEPER_MAX_VERTICES); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &creeper_head, true); + Model_DrawPart(&creeper_torso); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &creeper_leftLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &creeper_rightLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &creeper_leftLegBack, false); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &creeper_rightLegBack, false); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(CREEPER_MAX_VERTICES); +} + +static float CreeperModel_GetNameY(struct Entity* e) { return 1.7f; } +static float CreeperModel_GetEyeY(struct Entity* e) { return 22.0f/16.0f; } +static void CreeperModel_GetSize(struct Entity* e) { Model_RetSize(8.0f,26.0f,8.0f); } +static void CreeperModel_GetBounds(struct Entity* e) { Model_RetAABB(-4,0,-6, 4,26,6); } + +static struct ModelVertex creeper_vertices[MODEL_BOX_VERTICES * 6]; +static struct ModelTex creeper_tex = { "creeper.png" }; +static struct Model creeper_model = { + "creeper", creeper_vertices, &creeper_tex, + CreeperModel_MakeParts, CreeperModel_Draw, + CreeperModel_GetNameY, CreeperModel_GetEyeY, + CreeperModel_GetSize, CreeperModel_GetBounds +}; + +static void CreeperModel_Register(void) { + Model_Init(&creeper_model); + creeper_model.maxVertices = CREEPER_MAX_VERTICES; + Model_Register(&creeper_model); +} + + +/*########################################################################################################################* +*----------------------------------------------------------PigModel-------------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart pig_head, pig_torso, pig_leftLegFront, pig_rightLegFront; +static struct ModelPart pig_leftLegBack, pig_rightLegBack; +#define PIG_MAX_VERTICES (6 * MODEL_BOX_VERTICES) + +static void PigModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-4,8,-14, 4,16,-6), + BoxDesc_Rot(0,12,-6), + }; + static const struct BoxDesc torso = { + BoxDesc_Tex(28,8), + BoxDesc_Box(-5,6,-8, 5,14,8), + BoxDesc_Rot(0,6,0), + }; + static const struct BoxDesc lFront = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-5,0,-7, -1,6,-3), + BoxDesc_Rot(0,6,-5), + }; + static const struct BoxDesc rFront = { + BoxDesc_Tex(0,16), + BoxDesc_Box(1,0,-7, 5,6,-3), + BoxDesc_Rot(0,6,-5), + }; + static const struct BoxDesc lBack = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-5,0,5, -1,6,9), + BoxDesc_Rot(0,6,7), + }; + static const struct BoxDesc rBack = { + BoxDesc_Tex(0,16), + BoxDesc_Box(1,0,5, 5,6,9), + BoxDesc_Rot(0,6,7), + }; + + BoxDesc_BuildBox(&pig_head, &head); + BoxDesc_BuildRotatedBox(&pig_torso, &torso); + BoxDesc_BuildBox(&pig_leftLegFront, &lFront); + BoxDesc_BuildBox(&pig_rightLegFront, &rFront); + BoxDesc_BuildBox(&pig_leftLegBack, &lBack); + BoxDesc_BuildBox(&pig_rightLegBack, &rBack); +} + +static void PigModel_Draw(struct Entity* e) { + Model_ApplyTexture(e); + Model_LockVB(e, PIG_MAX_VERTICES); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &pig_head, true); + Model_DrawPart(&pig_torso); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &pig_leftLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &pig_rightLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &pig_leftLegBack, false); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &pig_rightLegBack, false); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(PIG_MAX_VERTICES); +} + +static float PigModel_GetNameY(struct Entity* e) { return 1.075f; } +static float PigModel_GetEyeY(struct Entity* e) { return 12.0f/16.0f; } +static void PigModel_GetSize(struct Entity* e) { Model_RetSize(14.0f,14.0f,14.0f); } +static void PigModel_GetBounds(struct Entity* e) { Model_RetAABB(-5,0,-14, 5,16,9); } + +static struct ModelVertex pig_vertices[MODEL_BOX_VERTICES * 6]; +static struct ModelTex pig_tex = { "pig.png" }; +static struct Model pig_model = { "pig", pig_vertices, &pig_tex, + PigModel_MakeParts, PigModel_Draw, + PigModel_GetNameY, PigModel_GetEyeY, + PigModel_GetSize, PigModel_GetBounds +}; + +static void PigModel_Register(void) { + Model_Init(&pig_model); + pig_model.maxVertices = PIG_MAX_VERTICES; + Model_Register(&pig_model); +} + + +/*########################################################################################################################* +*---------------------------------------------------------SheepModel------------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart sheep_head, sheep_torso, sheep_leftLegFront; +static struct ModelPart sheep_rightLegFront, sheep_leftLegBack, sheep_rightLegBack; +static struct ModelPart fur_head, fur_torso, fur_leftLegFront, fur_rightLegFront; +static struct ModelPart fur_leftLegBack, fur_rightLegBack; +static struct ModelTex fur_tex = { "sheep_fur.png" }; +#define SHEEP_BODY_VERTICES (6 * MODEL_BOX_VERTICES) +#define SHEEP_FUR_VERTICES (6 * MODEL_BOX_VERTICES) + +static void SheepModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-3,16,-14, 3,22,-6), + BoxDesc_Rot(0,18,-8), + }; + static const struct BoxDesc torso = { + BoxDesc_Tex(28,8), + BoxDesc_Box(-4,12,-8, 4,18,8), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc lFront = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-5,0,-7, -1,12,-3), + BoxDesc_Rot(0,12,-5), + }; + static const struct BoxDesc rFront = { + BoxDesc_Tex(0,16), + BoxDesc_Box(1,0,-7, 5,12,-3), + BoxDesc_Rot(0,12,-5), + }; + static const struct BoxDesc lBack = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-5,0,5, -1,12,9), + BoxDesc_Rot(0,12,7), + }; + static const struct BoxDesc rBack = { + BoxDesc_Tex(0,16), + BoxDesc_Box(1,0,5, 5,12,9), + BoxDesc_Rot(0,12,7), + }; + + static const struct BoxDesc fHead = { + BoxDesc_Tex(0,0), + BoxDesc_Dims(-3,16,-12, 3,22,-6), + BoxDesc_Bounds(-3.5f,15.5f,-12.5f, 3.5f,22.5f,-5.5f), + BoxDesc_Rot(0,18,-8), + }; + static const struct BoxDesc fTorso = { + BoxDesc_Tex(28,8), + BoxDesc_Dims(-4,12,-8, 4,18,8), + BoxDesc_Bounds(-6.0f,10.5f,-10.0f, 6.0f,19.5f,10.0f), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc flFront = { + BoxDesc_Tex(0,16), + BoxDesc_Dims(-5,6,-7, -1,12,-3), + BoxDesc_Bounds(-5.5f,5.5f,-7.5f, -0.5f,12.5f,-2.5f), + BoxDesc_Rot(0,12,-5), + }; + static const struct BoxDesc frFront = { + BoxDesc_Tex(0,16), + BoxDesc_Dims(1,6,-7, 5,12,-3), + BoxDesc_Bounds(0.5f,5.5f,-7.5f, 5.5f,12.5f,-2.5f), + BoxDesc_Rot(0,12,-5), + }; + static const struct BoxDesc flBack = { + BoxDesc_Tex(0,16), + BoxDesc_Dims(-5,6,5, -1,12,9), + BoxDesc_Bounds(-5.5f,5.5f,4.5f, -0.5f,12.5f,9.5f), + BoxDesc_Rot(0,12,7), + }; + static const struct BoxDesc frBack = { + BoxDesc_Tex(0,16), + BoxDesc_Dims(1,6,5, 5,12,9), + BoxDesc_Bounds(0.5f,5.5f,4.5f, 5.5f,12.5f,9.5f), + BoxDesc_Rot(0,12,7), + }; + + BoxDesc_BuildBox(&sheep_head, &head); + BoxDesc_BuildRotatedBox(&sheep_torso, &torso); + BoxDesc_BuildBox(&sheep_leftLegFront, &lFront); + BoxDesc_BuildBox(&sheep_rightLegFront, &rFront); + BoxDesc_BuildBox(&sheep_leftLegBack, &lBack); + BoxDesc_BuildBox(&sheep_rightLegBack, &rBack); + + BoxDesc_BuildBox(&fur_head, &fHead); + BoxDesc_BuildRotatedBox(&fur_torso, &fTorso); + BoxDesc_BuildBox(&fur_leftLegFront, &flFront); + BoxDesc_BuildBox(&fur_rightLegFront, &frFront); + BoxDesc_BuildBox(&fur_leftLegBack, &flBack); + BoxDesc_BuildBox(&fur_rightLegBack, &frBack); +} + +static void SheepModel_DrawBody(struct Entity* e) { + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &sheep_head, true); + Model_DrawPart(&sheep_torso); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &sheep_leftLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &sheep_rightLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &sheep_leftLegBack, false); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &sheep_rightLegBack, false); +} + +static void FurlessModel_Draw(struct Entity* e) { + Model_ApplyTexture(e); + Model_LockVB(e, SHEEP_BODY_VERTICES); + + SheepModel_DrawBody(e); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(SHEEP_BODY_VERTICES); +} + +static void SheepModel_Draw(struct Entity* e) { + Model_ApplyTexture(e); + Model_LockVB(e, SHEEP_BODY_VERTICES + SHEEP_FUR_VERTICES); + + SheepModel_DrawBody(e); + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &fur_head, true); + Model_DrawPart(&fur_torso); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &fur_leftLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &fur_rightLegFront, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &fur_leftLegBack, false); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &fur_rightLegBack, false); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(SHEEP_BODY_VERTICES); + Gfx_BindTexture(fur_tex.texID); + Gfx_DrawVb_IndexedTris_Range(SHEEP_FUR_VERTICES, SHEEP_BODY_VERTICES); +} + +static float SheepModel_GetNameY(struct Entity* e) { return 1.48125f; } +static float SheepModel_GetEyeY(struct Entity* e) { return 20.0f/16.0f; } +static void SheepModel_GetSize(struct Entity* e) { Model_RetSize(10.0f,20.0f,10.0f); } +static void SheepModel_GetBounds(struct Entity* e) { Model_RetAABB(-6,0,-13, 6,23,10); } + +static struct ModelVertex sheep_vertices[MODEL_BOX_VERTICES * 6 * 2]; +static struct ModelTex sheep_tex = { "sheep.png" }; +static struct Model sheep_model = { "sheep", sheep_vertices, &sheep_tex, + SheepModel_MakeParts, SheepModel_Draw, + SheepModel_GetNameY, SheepModel_GetEyeY, + SheepModel_GetSize, SheepModel_GetBounds +}; +static struct Model nofur_model = { "sheep_nofur", sheep_vertices, &sheep_tex, + SheepModel_MakeParts, FurlessModel_Draw, + SheepModel_GetNameY, SheepModel_GetEyeY, + SheepModel_GetSize, SheepModel_GetBounds +}; + +static void SheepModel_Register(void) { + Model_Init(&sheep_model); + sheep_model.maxVertices = SHEEP_BODY_VERTICES + SHEEP_FUR_VERTICES; + Model_Register(&sheep_model); +} + +static void NoFurModel_Register(void) { + Model_Init(&nofur_model); + nofur_model.maxVertices = SHEEP_BODY_VERTICES; + Model_Register(&nofur_model); +} + + +/*########################################################################################################################* +*-------------------------------------------------------SkeletonModel-----------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart skeleton_head, skeleton_torso, skeleton_leftLeg; +static struct ModelPart skeleton_rightLeg, skeleton_leftArm, skeleton_rightArm; +#define SKELETON_MAX_VERTICES (6 * MODEL_BOX_VERTICES) + +static void SkeletonModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-4,24,-4, 4,32,4), + BoxDesc_Rot(0,24,0), + }; + static const struct BoxDesc torso = { + BoxDesc_Tex(16,16), + BoxDesc_Box(-4,12,-2, 4,24,2), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc lLeg = { + BoxDesc_Tex(0,16), + BoxDesc_Box(-1,0,-1, -3,12,1), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc rLeg = { + BoxDesc_Tex(0,16), + BoxDesc_Box(1,0,-1, 3,12,1), + BoxDesc_Rot(0,12,0), + }; + static const struct BoxDesc lArm = { + BoxDesc_Tex(40,16), + BoxDesc_Box(-4,12,-1, -6,24,1), + BoxDesc_Rot(-5,23,0), + }; + static const struct BoxDesc rArm = { + BoxDesc_Tex(40,16), + BoxDesc_Box(4,12,-1, 6,24,1), + BoxDesc_Rot(5,23,0), + }; + + BoxDesc_BuildBox(&skeleton_head, &head); + BoxDesc_BuildBox(&skeleton_torso, &torso); + BoxDesc_BuildBox(&skeleton_leftLeg, &lLeg); + BoxDesc_BuildBox(&skeleton_rightLeg, &rLeg); + BoxDesc_BuildBox(&skeleton_leftArm, &lArm); + BoxDesc_BuildBox(&skeleton_rightArm, &rArm); +} + +static void SkeletonModel_Draw(struct Entity* e) { + Model_ApplyTexture(e); + Model_LockVB(e, SKELETON_MAX_VERTICES); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &skeleton_head, true); + Model_DrawPart(&skeleton_torso); + Model_DrawRotate(e->Anim.LeftLegX, 0, 0, &skeleton_leftLeg, false); + Model_DrawRotate(e->Anim.RightLegX, 0, 0, &skeleton_rightLeg, false); + Model_DrawRotate(90.0f * MATH_DEG2RAD, 0, e->Anim.LeftArmZ, &skeleton_leftArm, false); + Model_DrawRotate(90.0f * MATH_DEG2RAD, 0, e->Anim.RightArmZ, &skeleton_rightArm, false); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(SKELETON_MAX_VERTICES); +} + +static void SkeletonModel_DrawArm(struct Entity* e) { + Gfx_SetAlphaTest(true); + Model_LockVB(e, MODEL_BOX_VERTICES); + + Model_DrawArmPart(&skeleton_rightArm); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(MODEL_BOX_VERTICES); +} + +static void SkeletonModel_GetSize(struct Entity* e) { Model_RetSize(8.0f,28.1f,8.0f); } +static void SkeletonModel_GetBounds(struct Entity* e) { Model_RetAABB(-4,0,-4, 4,32,4); } + +static struct ModelVertex skeleton_vertices[MODEL_BOX_VERTICES * 6]; +static struct ModelTex skeleton_tex = { "skeleton.png" }; +static struct Model skeleton_model = { "skeleton", skeleton_vertices, &skeleton_tex, + SkeletonModel_MakeParts, SkeletonModel_Draw, + HumanModel_GetNameY, HumanModel_GetEyeY, + SkeletonModel_GetSize, SkeletonModel_GetBounds +}; + +static void SkeletonModel_Register(void) { + Model_Init(&skeleton_model); + skeleton_model.DrawArm = SkeletonModel_DrawArm; + skeleton_model.armX = 5; + skeleton_model.maxVertices = SKELETON_MAX_VERTICES; + Model_Register(&skeleton_model); +} + + +/*########################################################################################################################* +*--------------------------------------------------------SpiderModel------------------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart spider_head, spider_link, spider_end; +static struct ModelPart spider_leftLeg, spider_rightLeg; +#define SPIDER_MAX_VERTICES (11 * MODEL_BOX_VERTICES) + +static void SpiderModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(32,4), + BoxDesc_Box(-4,4,-11, 4,12,-3), + BoxDesc_Rot(0,8,-3), + }; + static const struct BoxDesc link = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-3,5,3, 3,11,-3), + BoxDesc_Rot(0,5,0), + }; + static const struct BoxDesc end = { + BoxDesc_Tex(0,12), + BoxDesc_Box(-5,4,3, 5,12,15), + BoxDesc_Rot(0,4,9), + }; + static const struct BoxDesc lLeg = { + BoxDesc_Tex(18,0), + BoxDesc_Box(-19,7,-1, -3,9,1), + BoxDesc_Rot(-3,8,0), + }; + static const struct BoxDesc rLeg = { + BoxDesc_Tex(18,0), + BoxDesc_Box(3,7,-1, 19,9,1), + BoxDesc_Rot(3,8,0), + }; + + BoxDesc_BuildBox(&spider_head, &head); + BoxDesc_BuildBox(&spider_link, &link); + BoxDesc_BuildBox(&spider_end, &end); + BoxDesc_BuildBox(&spider_leftLeg, &lLeg); + BoxDesc_BuildBox(&spider_rightLeg, &rLeg); +} + +#define quarterPi (MATH_PI / 4.0f) +#define eighthPi (MATH_PI / 8.0f) + +static void SpiderModel_Draw(struct Entity* e) { + float rotX, rotY, rotZ; + Model_ApplyTexture(e); + Model_LockVB(e, SPIDER_MAX_VERTICES); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &spider_head, true); + Model_DrawPart(&spider_link); + Model_DrawPart(&spider_end); + + rotX = Math_SinF(e->Anim.WalkTime) * e->Anim.Swing * MATH_PI; + rotZ = Math_CosF(e->Anim.WalkTime * 2) * e->Anim.Swing * MATH_PI / 16.0f; + rotY = Math_SinF(e->Anim.WalkTime * 2) * e->Anim.Swing * MATH_PI / 32.0f; + Models.Rotation = ROTATE_ORDER_XZY; + + Model_DrawRotate(rotX, quarterPi + rotY, eighthPi + rotZ, &spider_leftLeg, false); + Model_DrawRotate(-rotX, eighthPi + rotY, eighthPi + rotZ, &spider_leftLeg, false); + Model_DrawRotate(rotX, -eighthPi - rotY, eighthPi - rotZ, &spider_leftLeg, false); + Model_DrawRotate(-rotX, -quarterPi - rotY, eighthPi - rotZ, &spider_leftLeg, false); + + Model_DrawRotate(rotX, -quarterPi + rotY, -eighthPi + rotZ, &spider_rightLeg, false); + Model_DrawRotate(-rotX, -eighthPi + rotY, -eighthPi + rotZ, &spider_rightLeg, false); + Model_DrawRotate(rotX, eighthPi - rotY, -eighthPi - rotZ, &spider_rightLeg, false); + Model_DrawRotate(-rotX, quarterPi - rotY, -eighthPi - rotZ, &spider_rightLeg, false); + + Models.Rotation = ROTATE_ORDER_ZYX; + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(SPIDER_MAX_VERTICES); +} + +static float SpiderModel_GetNameY(struct Entity* e) { return 1.0125f; } +static float SpiderModel_GetEyeY(struct Entity* e) { return 8.0f/16.0f; } +static void SpiderModel_GetSize(struct Entity* e) { Model_RetSize(15.0f,12.0f,15.0f); } +static void SpiderModel_GetBounds(struct Entity* e) { Model_RetAABB(-5,0,-11, 5,12,15); } + +static struct ModelVertex spider_vertices[MODEL_BOX_VERTICES * 5]; +static struct ModelTex spider_tex = { "spider.png" }; +static struct Model spider_model = { "spider", spider_vertices, &spider_tex, + SpiderModel_MakeParts, SpiderModel_Draw, + SpiderModel_GetNameY, SpiderModel_GetEyeY, + SpiderModel_GetSize, SpiderModel_GetBounds +}; + +static void SpiderModel_Register(void) { + Model_Init(&spider_model); + spider_model.maxVertices = SPIDER_MAX_VERTICES; + Model_Register(&spider_model); +} + + +/*########################################################################################################################* +*--------------------------------------------------------ZombieModel------------------------------------------------------* +*#########################################################################################################################*/ +static void ZombieModel_Draw(struct Entity* e) { + e->Anim.LeftArmX = 90.0f * MATH_DEG2RAD; + e->Anim.RightArmX = 90.0f * MATH_DEG2RAD; + HumanModel_DrawCore(e, &human_set, false); +} +static void ZombieModel_DrawArm(struct Entity* e) { + HumanModel_DrawArmCore(e, &human_set); +} + +static void ZombieModel_GetBounds(struct Entity* e) { Model_RetAABB(-4,0,-4, 4,32,4); } + +static struct ModelTex zombie_tex = { "zombie.png" }; +static struct Model zombie_model = { "zombie", human_vertices, &zombie_tex, + Model_NoParts, ZombieModel_Draw, + HumanModel_GetNameY, HumanModel_GetEyeY, + HumanModel_GetSize, ZombieModel_GetBounds +}; + +static void ZombieModel_Register(void) { + Model_Init(&zombie_model); + zombie_model.DrawArm = ZombieModel_DrawArm; + zombie_model.maxVertices = HUMAN_MAX_VERTICES; + Model_Register(&zombie_model); +} + + +/*########################################################################################################################* +*---------------------------------------------------------BlockModel------------------------------------------------------* +*#########################################################################################################################*/ +#define BLOCKMODEL_SPRITE_COUNT (8 * 4) +#define BLOCKMODEL_CUBE_COUNT (6 * 4) +#define BLOCKMODEL_MAX_VERTICES BLOCKMODEL_SPRITE_COUNT + +static BlockID bModel_block = BLOCK_AIR; +static int bModel_index, bModel_texIndices[8]; +static struct VertexTextured* bModel_vertices; + +static float BlockModel_GetNameY(struct Entity* e) { + BlockID block = e->ModelBlock; + return Blocks.MaxBB[block].y + 0.075f; +} + +static float BlockModel_GetEyeY(struct Entity* e) { + BlockID block = e->ModelBlock; + float minY = Blocks.MinBB[block].y; + float maxY = Blocks.MaxBB[block].y; + return (minY + maxY) / 2.0f; +} + +static void BlockModel_GetSize(struct Entity* e) { + static Vec3 shrink = { 0.75f/16.0f, 0.75f/16.0f, 0.75f/16.0f }; + Vec3* size = &e->Size; + BlockID block = e->ModelBlock; + + Vec3_Sub(size, &Blocks.MaxBB[block], &Blocks.MinBB[block]); + /* to fit slightly inside */ + Vec3_SubBy(size, &shrink); + + /* fix for 0 size blocks */ + size->x = max(size->x, 0.125f/16.0f); + size->y = max(size->y, 0.125f/16.0f); + size->z = max(size->z, 0.125f/16.0f); +} + +static void BlockModel_GetBounds(struct Entity* e) { + static Vec3 offset = { -0.5f, 0.0f, -0.5f }; + BlockID block = e->ModelBlock; + + Vec3_Add(&e->ModelAABB.Min, &Blocks.MinBB[block], &offset); + Vec3_Add(&e->ModelAABB.Max, &Blocks.MaxBB[block], &offset); +} + +static TextureLoc BlockModel_GetTex(Face face) { + TextureLoc loc = Block_Tex(bModel_block, face); + bModel_texIndices[bModel_index++] = Atlas1D_Index(loc); + return loc; +} + +static void BlockModel_SpriteZQuad(cc_bool firstPart, cc_bool mirror) { + struct VertexTextured* ptr, v; + PackedCol col; int tmp; + float xz1, xz2; + TextureLoc loc = BlockModel_GetTex(FACE_ZMAX); + TextureRec rec = Atlas1D_TexRec(loc, 1, &tmp); + + col = Models.Cols[0]; + Block_Tint(col, bModel_block); + + xz1 = 0.0f; xz2 = 0.0f; + if (firstPart) { /* Need to break into two quads for when drawing a sprite model in hand. */ + if (mirror) { rec.u1 = 0.5f; xz1 = -5.5f/16.0f; } + else { rec.u2 = 0.5f; xz2 = -5.5f/16.0f; } + } else { + if (mirror) { rec.u2 = 0.5f; xz2 = 5.5f/16.0f; } + else { rec.u1 = 0.5f; xz1 = 5.5f/16.0f; } + } + + ptr = bModel_vertices; + v.Col = col; + + v.x = xz1; v.y = 0.0f; v.z = xz1; v.U = rec.u2; v.V = rec.v2; *ptr++ = v; + v.y = 1.0f; v.V = rec.v1; *ptr++ = v; + v.x = xz2; v.z = xz2; v.U = rec.u1; *ptr++ = v; + v.y = 0.0f; v.V = rec.v2; *ptr++ = v; + + bModel_vertices = ptr; +} + +static void BlockModel_SpriteXQuad(cc_bool firstPart, cc_bool mirror) { + struct VertexTextured* ptr, v; + PackedCol col; int tmp; + float x1, x2, z1, z2; + TextureLoc loc = BlockModel_GetTex(FACE_XMAX); + TextureRec rec = Atlas1D_TexRec(loc, 1, &tmp); + + col = Models.Cols[0]; + Block_Tint(col, bModel_block); + + x1 = 0.0f; x2 = 0.0f; z1 = 0.0f; z2 = 0.0f; + if (firstPart) { + if (mirror) { rec.u2 = 0.5f; x2 = -5.5f/16.0f; z2 = 5.5f/16.0f; } + else { rec.u1 = 0.5f; x1 = -5.5f/16.0f; z1 = 5.5f/16.0f; } + } else { + if (mirror) { rec.u1 = 0.5f; x1 = 5.5f/16.0f; z1 = -5.5f/16.0f; } + else { rec.u2 = 0.5f; x2 = 5.5f/16.0f; z2 = -5.5f/16.0f; } + } + + ptr = bModel_vertices; + v.Col = col; + + v.x = x1; v.y = 0.0f; v.z = z1; v.U = rec.u2; v.V = rec.v2; *ptr++ = v; + v.y = 1.0f; v.V = rec.v1; *ptr++ = v; + v.x = x2; v.z = z2; v.U = rec.u1; *ptr++ = v; + v.y = 0.0f; v.V = rec.v2; *ptr++ = v; + + bModel_vertices = ptr; +} + +static void BlockModel_BuildParts(struct Entity* e, cc_bool sprite) { + struct VertexTextured* ptr; + Vec3 min, max; + TextureLoc loc; + + Model_LockVB(e, sprite ? BLOCKMODEL_SPRITE_COUNT : BLOCKMODEL_CUBE_COUNT); + ptr = Models.Vertices; + + if (sprite) { + bModel_vertices = ptr; + + BlockModel_SpriteXQuad(false, false); + BlockModel_SpriteXQuad(false, true); + BlockModel_SpriteZQuad(false, false); + BlockModel_SpriteZQuad(false, true); + + BlockModel_SpriteZQuad(true, false); + BlockModel_SpriteZQuad(true, true); + BlockModel_SpriteXQuad(true, false); + BlockModel_SpriteXQuad(true, true); + } else { + Drawer.MinBB = Blocks.MinBB[bModel_block]; Drawer.MinBB.y = 1.0f - Drawer.MinBB.y; + Drawer.MaxBB = Blocks.MaxBB[bModel_block]; Drawer.MaxBB.y = 1.0f - Drawer.MaxBB.y; + Drawer.Tinted = Blocks.Tinted[bModel_block]; + Drawer.TintCol = Blocks.FogCol[bModel_block]; + + min = Blocks.RenderMinBB[bModel_block]; + max = Blocks.RenderMaxBB[bModel_block]; + + Drawer.X1 = min.x - 0.5f; Drawer.Y1 = min.y; Drawer.Z1 = min.z - 0.5f; + Drawer.X2 = max.x - 0.5f; Drawer.Y2 = max.y; Drawer.Z2 = max.z - 0.5f; + + loc = BlockModel_GetTex(FACE_YMIN); Drawer_YMin(1, Models.Cols[1], loc, &ptr); + loc = BlockModel_GetTex(FACE_ZMIN); Drawer_ZMin(1, Models.Cols[3], loc, &ptr); + loc = BlockModel_GetTex(FACE_XMAX); Drawer_XMax(1, Models.Cols[5], loc, &ptr); + loc = BlockModel_GetTex(FACE_ZMAX); Drawer_ZMax(1, Models.Cols[2], loc, &ptr); + loc = BlockModel_GetTex(FACE_XMIN); Drawer_XMin(1, Models.Cols[4], loc, &ptr); + loc = BlockModel_GetTex(FACE_YMAX); Drawer_YMax(1, Models.Cols[0], loc, &ptr); + } + + Model_UnlockVB(); +} + +static void BlockModel_DrawParts(void) { + int lastTexIndex, i, offset = 0, count = 0; + + lastTexIndex = bModel_texIndices[0]; + for (i = 0; i < bModel_index; i++, count += 4) { + if (bModel_texIndices[i] == lastTexIndex) continue; + + /* Different 1D flush texture, flush current vertices */ + Gfx_BindTexture(Atlas1D.TexIds[lastTexIndex]); + Gfx_DrawVb_IndexedTris_Range(count, offset); + lastTexIndex = bModel_texIndices[i]; + + offset += count; + count = 0; + } + + /* Leftover vertices */ + if (!count) return; + Gfx_BindTexture(Atlas1D.TexIds[lastTexIndex]); + Gfx_DrawVb_IndexedTris_Range(count, offset); +} + +static void BlockModel_Draw(struct Entity* e) { + cc_bool sprite; + int i; + + bModel_block = e->ModelBlock; + bModel_index = 0; + if (Blocks.Draw[bModel_block] == DRAW_GAS) return; + + if (Blocks.Brightness[bModel_block]) { + for (i = 0; i < FACE_COUNT; i++) + { + Models.Cols[i] = PACKEDCOL_WHITE; + } + } + + sprite = Blocks.Draw[bModel_block] == DRAW_SPRITE; + BlockModel_BuildParts(e, sprite); + + if (sprite) Gfx_SetFaceCulling(true); + BlockModel_DrawParts(); + if (sprite) Gfx_SetFaceCulling(false); +} + +static struct Model block_model = { "block", NULL, &human_tex, + Model_NoParts, BlockModel_Draw, + BlockModel_GetNameY, BlockModel_GetEyeY, + BlockModel_GetSize, BlockModel_GetBounds, +}; + +static void BlockModel_Register(void) { + Model_Init(&block_model); + block_model.bobbing = false; + block_model.usesSkin = false; + block_model.pushes = false; + block_model.maxVertices = BLOCKMODEL_MAX_VERTICES; + Model_Register(&block_model); + Models.Block = &block_model; +} + + +/*########################################################################################################################* +*----------------------------------------------------------SkinnedCubeModel-----------------------------------------------* +*#########################################################################################################################*/ +static struct ModelPart skinnedCube_head; +#define SKINNEDCUBE_MAX_VERTICES (1 * MODEL_BOX_VERTICES) + +static void SkinnedCubeModel_MakeParts(void) { + static const struct BoxDesc head = { + BoxDesc_Tex(0,0), + BoxDesc_Box(-8,0,-8, 8,16,8), + BoxDesc_Rot(0,8,0), + }; + + BoxDesc_BuildBox(&skinnedCube_head, &head); +} + +static void SkinnedCubeModel_Draw(struct Entity* e) { + Model_ApplyTexture(e); + Model_LockVB(e, SKINNEDCUBE_MAX_VERTICES); + + Model_DrawRotate(-e->Pitch * MATH_DEG2RAD, 0, 0, &skinnedCube_head, true); + + Model_UnlockVB(); + Gfx_DrawVb_IndexedTris(SKINNEDCUBE_MAX_VERTICES); +} + +static float SkinnedCubeModel_GetNameY(struct Entity* e) { return 1.075f; } +static float SkinnedCubeModel_GetEyeY(struct Entity* e) { return 8.0f / 16.0f; } +static void SkinnedCubeModel_GetSize(struct Entity* e) { Model_RetSize(15.0f, 15.0f, 15.0f); } +static void SkinnedCubeModel_GetBounds(struct Entity* e) { Model_RetAABB(-8, 0, -8, 8, 16, 8); } + +static struct ModelVertex skinnedCube_vertices[MODEL_BOX_VERTICES]; +static struct ModelTex skinnedCube_tex = { "skinnedcube.png" }; +static struct Model skinnedCube_model = { "skinnedcube", skinnedCube_vertices, &skinnedCube_tex, + SkinnedCubeModel_MakeParts, SkinnedCubeModel_Draw, + SkinnedCubeModel_GetNameY, SkinnedCubeModel_GetEyeY, + SkinnedCubeModel_GetSize, SkinnedCubeModel_GetBounds +}; + +static void SkinnedCubeModel_Register(void) { + Model_Init(&skinnedCube_model); + skinnedCube_model.usesHumanSkin = true; + skinnedCube_model.pushes = false; + skinnedCube_model.maxVertices = SKINNEDCUBE_MAX_VERTICES; + Model_Register(&skinnedCube_model); +} + + + +/*########################################################################################################################* +*---------------------------------------------------------HoldModel-------------------------------------------------------* +*#########################################################################################################################*/ +static void RecalcProperties(struct Entity* e) { + // Calculate block ID based on the X scale of the model + // E.g, hold|1.001 = stone (ID = 1), hold|1.041 = gold (ID = 41) etc. + BlockID block = (BlockID)((e->ModelScale.x - 0.9999f) * 1000); + + if (block > 0) { + // Change the block that the player is holding + if (block < BLOCK_COUNT) e->ModelBlock = block; + else e->ModelBlock = BLOCK_AIR; + + Vec3_Set(e->ModelScale, 1, 1, 1); + Entity_UpdateModelBounds(e); // Adjust size/modelAABB after changing model scale + } +} + +static void DrawBlockTransform(struct Entity* e, float dispX, float dispY, float dispZ, float scale) { + static Vec3 pos; + static struct Matrix m, temp; + + pos = e->Position; + pos.y += e->Anim.BobbingModel; + + Entity_GetTransform(e, pos, e->ModelScale, &m); + Matrix_Mul(&m, &m, &Gfx.View); + Matrix_Translate(&temp, dispX, dispY, dispZ); + Matrix_Mul(&m, &temp, &m); + Matrix_Scale(&temp, scale / 1.5f, scale / 1.5f, scale / 1.5f); + Matrix_Mul(&m, &temp, &m); + + Model_SetupState(&block_model, e); + Gfx_LoadMatrix(MATRIX_VIEW, &m); + block_model.Draw(e); +} + +static void HoldModel_Draw(struct Entity* e) { + float handBob; + float handIdle; + RecalcProperties(e); + + handBob = Math_SinF(e->Anim.WalkTime * 2.0f) * e->Anim.Swing * MATH_PI / 16.0f; + handIdle = e->Anim.RightArmX * (1.0f - e->Anim.Swing); + + e->Anim.RightArmX = 0.5f + handBob + handIdle; + e->Anim.RightArmZ = 0; + + Model_SetupState(Models.Human, e); + HumanModel_Draw(e); + + DrawBlockTransform(e, 0.33F, (MATH_PI / 3 + handBob + handIdle) * 10 / 16 + 0.127f, -7.0f / 16, 0.5f); +} + +static struct Model hold_model; + +static float HoldModel_GetEyeY(struct Entity* e) { + RecalcProperties(e); + return HumanModel_GetEyeY(e); +} + +static void HoldModel_Register(void) { + hold_model = human_model; + hold_model.name = "hold"; + hold_model.MakeParts = Model_NoParts; + hold_model.Draw = HoldModel_Draw; + hold_model.GetEyeY = HoldModel_GetEyeY; + Model_Register(&hold_model); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Models component--------------------------------------------------* +*#########################################################################################################################*/ +static void RegisterDefaultModels(void) { + Model_RegisterTexture(&human_tex); + Model_RegisterTexture(&chicken_tex); + Model_RegisterTexture(&creeper_tex); + Model_RegisterTexture(&pig_tex); + Model_RegisterTexture(&sheep_tex); + Model_RegisterTexture(&fur_tex); + Model_RegisterTexture(&skeleton_tex); + Model_RegisterTexture(&spider_tex); + Model_RegisterTexture(&zombie_tex); + Model_RegisterTexture(&skinnedCube_tex); + + HumanoidModel_Register(); + MakeModel(&human_model); + Models.Human = &human_model; + BlockModel_Register(); + + ChickenModel_Register(); + CreeperModel_Register(); + PigModel_Register(); + SheepModel_Register(); + NoFurModel_Register(); + SkeletonModel_Register(); + SpiderModel_Register(); + ZombieModel_Register(); + + ChibiModel_Register(); + HeadModel_Register(); + SittingModel_Register(); + CorpseModel_Register(); + SkinnedCubeModel_Register(); + HoldModel_Register(); +} + +static void OnContextLost(void* obj) { + struct ModelTex* tex; + Gfx_DeleteDynamicVb(&Models.Vb); + if (Gfx.ManagedTextures) return; + + for (tex = textures_head; tex; tex = tex->next) + { + Gfx_DeleteTexture(&tex->texID); + } +} + +static void OnInit(void) { + Models.MaxVertices = MODELS_MAX_VERTICES; + RegisterDefaultModels(); + Models.ClassicArms = Options_GetBool(OPT_CLASSIC_ARM_MODEL, Game_ClassicMode); + + Event_Register_(&TextureEvents.FileChanged, NULL, Models_TextureChanged); + Event_Register_(&GfxEvents.ContextLost, NULL, OnContextLost); +} + +static void OnFree(void) { + OnContextLost(NULL); + CustomModel_FreeAll(); +} + +static void OnReset(void) { CustomModel_FreeAll(); } + +struct IGameComponent Models_Component = { + OnInit, /* Init */ + OnFree, /* Free */ + OnReset, /* Reset */ +}; diff --git a/src/Model.h b/src/Model.h new file mode 100644 index 0000000..6b48b07 --- /dev/null +++ b/src/Model.h @@ -0,0 +1,300 @@ +#ifndef CC_MODEL_H +#define CC_MODEL_H +#include "Vectors.h" +#include "PackedCol.h" +#include "Constants.h" +#include "Physics.h" +/* Contains various structs and methods for an entity model. + Also contains a list of models and default textures for those models. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Entity; +struct AABB; +struct IGameComponent; +struct VertexTextured; +extern struct IGameComponent Models_Component; + +#define MODEL_QUAD_VERTICES 4 +#define MODEL_BOX_VERTICES (FACE_COUNT * MODEL_QUAD_VERTICES) +enum RotateOrder { ROTATE_ORDER_ZYX, ROTATE_ORDER_XZY, ROTATE_ORDER_YZX, ROTATE_ORDER_XYZ }; + +/* Describes a vertex within a model. */ +struct ModelVertex { float x, y, z; cc_uint16 u, v; }; +static CC_INLINE void ModelVertex_Init(struct ModelVertex* vertex, float x, float y, float z, int u, int v) { + vertex->x = x; vertex->y = y; vertex->z = z; + vertex->u = u; vertex->v = v; +} + +/* Describes the starting index of this part within a model's array of vertices, +and the number of vertices following the starting index that this part uses. */ +struct ModelPart { cc_uint16 offset, count; float rotX, rotY, rotZ; }; +static CC_INLINE void ModelPart_Init(struct ModelPart* part, int offset, int count, float rotX, float rotY, float rotZ) { + part->offset = offset; part->count = count; + part->rotX = rotX; part->rotY = rotY; part->rotZ = rotZ; +} + +struct ModelTex; +/* Contains information about a texture used for models. */ +struct ModelTex { const char* name; cc_uint8 skinType; GfxResourceID texID; struct ModelTex* next; }; + +#define MODEL_FLAG_INITED 0x01 +#define MODEL_FLAG_CLEAR_HAT 0x02 + +struct Model; +/* Contains a set of quads and/or boxes that describe a 3D object as well as +the bounding boxes that contain the entire set of quads and/or boxes. */ +struct Model { + /* Name of this model */ + const char* name; + /* Pointer to the raw vertices of the model */ + struct ModelVertex* vertices; + /* Pointer to default texture for this model */ + struct ModelTex* defaultTex; + + /* Creates the ModelParts of this model and fills out vertices. */ + void (*MakeParts)(void); + /* Draws/Renders this model for the given entity. */ + void (*Draw)(struct Entity* entity); + /* Returns height the 'nametag' gets drawn at above the entity's feet. */ + float (*GetNameY)(struct Entity* entity); + /* Returns height the 'eye' is located at above the entity's feet. */ + float (*GetEyeY)(struct Entity* entity); + /* Sets entity->Size to the collision size of this model. */ + void (*GetCollisionSize)(struct Entity* entity); + /* Sets entity->ModelAABB to the 'picking' bounds of this model. */ + /* This is the AABB around the entity in which mouse clicks trigger 'interaction'. */ + /* NOTE: These bounds are not transformed. (i.e. no rotation, centered around 0,0,0) */ + void (*GetPickingBounds)(struct Entity* entity); + + /* The rest of the fields are set in Model_Init() */ + int index; + cc_uint8 armX, armY; /* these translate arm model part back to (0, 0) */ + + cc_uint8 flags; + /* Whether the model should be slightly bobbed up and down when rendering. */ + /* e.g. for HumanoidModel, when legs are at the peak of their swing, whole model is moved slightly down */ + cc_bool bobbing; + cc_bool usesSkin, calcHumanAnims, usesHumanSkin, pushes; + + float gravity; Vec3 drag, groundFriction; + + /* Returns the transformation matrix applied to the model when rendering. */ + /* NOTE: Most models just use Entity_GetTransform (except SittingModel) */ + void (*GetTransform)(struct Entity* entity, Vec3 pos, struct Matrix* m); + void (*DrawArm)(struct Entity* entity); + + float maxScale, shadowScale; + int maxVertices; + struct Model* next; +}; + +/* Shared data for models. */ +CC_VAR extern struct _ModelsData { + /* Tint colour applied to the faces of model parts. */ + PackedCol Cols[FACE_COUNT]; + /* U/V scale applied to skin texture when rendering models. */ + /* Default uScale is 1/32, vScale is 1/32 or 1/64 depending on skin. */ + float uScale, vScale; + /* Angle of offset of head from body rotation */ + float cosHead, sinHead; + /* Order of axes rotation when rendering parts. */ + cc_uint8 Rotation; + /* Skin type of current skin texture. */ + cc_uint8 skinType; + /* Whether to render arms like vanilla Minecraft Classic. */ + cc_bool ClassicArms; + /* Model currently being built or rendered. */ + struct Model* Active; + /* Dynamic vertex buffer for uploading model vertices. */ + GfxResourceID Vb; + /* Temporary storage for vertices. */ + struct VertexTextured* Vertices; + /* Maximum number of vertices that can be stored in Vertices. */ + /* NOTE: If you change this, you MUST also destroy and recreate the dynamic VB. */ + int MaxVertices; + /* Pointer to humanoid/human model */ + struct Model* Human; + /* Pointer to block model */ + struct Model* Block; +} Models; + +/* Initialises fields of a model to default. */ +CC_API void Model_Init(struct Model* model); + +/* Whether the bounding sphere of the model is currently visible. */ +cc_bool Model_ShouldRender(struct Entity* entity); +/* Approximately how far the given entity is away from the player. */ +float Model_RenderDistance(struct Entity* entity); +/* Draws the given entity as the given model. */ +CC_API void Model_Render(struct Model* model, struct Entity* entity); +/* Sets up state to be suitable for rendering the given model. */ +/* NOTE: Model_Render already calls this, you don't normally need to call this. */ +CC_API void Model_SetupState(struct Model* model, struct Entity* entity); +/* Applies the skin texture of the given entity to the model. */ +/* Uses model's default texture if the entity doesn't have a custom skin. */ +CC_API void Model_ApplyTexture(struct Entity* entity); + +/* Flushes buffered vertices to the GPU. */ +CC_API void Model_UpdateVB(void); +void Model_LockVB(struct Entity* entity, int verticesCount); +void Model_UnlockVB(void); + +/* Draws the given part with no part-specific rotation (e.g. torso). */ +CC_API void Model_DrawPart(struct ModelPart* part); +/* Draws the given part with rotation around part's rotation origin. (e.g. arms, head) */ +CC_API void Model_DrawRotate(float angleX, float angleY, float angleZ, struct ModelPart* part, cc_bool head); +/* Renders the 'arm' of a model. */ +void Model_RenderArm(struct Model* model, struct Entity* entity); +/* Draws the given part with appropriate rotation to produce an arm look. */ +CC_API void Model_DrawArmPart(struct ModelPart* part); + +/* Returns a pointer to the model whose name caselessly matches given name. */ +CC_API struct Model* Model_Get(const cc_string* name); +/* Adds a model to the list of models. (e.g. "skeleton") */ +/* Models can be applied to entities to change their appearance. Use Entity_SetModel for that. */ +CC_API void Model_Register(struct Model* model); +/* Unregister a model from the list of models, and set all entities using this model to the default humanoid model. */ +void Model_Unregister(struct Model* model); +/* Adds a texture to the list of automatically managed model textures. */ +/* These textures are automatically loaded from texture packs. (e.g. "skeleton.png") */ +CC_API void Model_RegisterTexture(struct ModelTex* tex); + +/* Describes data for a box being built. */ +struct BoxDesc { + cc_uint16 texX, texY; /* Texture origin */ + cc_uint8 sizeX, sizeY, sizeZ; /* Texture dimensions */ + float x1,y1,z1, x2,y2,z2; /* Box corners coordinates */ + float rotX,rotY,rotZ; /* Rotation origin point */ +}; + +#define BoxDesc_Dim(p1, p2) p1 < p2 ? p2 - p1 : p1 - p2 +/* Macros for making initialising a BoxDesc easier to understand. See Model.c for how these get used. */ +#define BoxDesc_Tex(x, y) x,y +#define BoxDesc_Dims(x1,y1,z1,x2,y2,z2) BoxDesc_Dim(x1,x2), BoxDesc_Dim(y1,y2), BoxDesc_Dim(z1,z2) +#define BoxDesc_Bounds(x1,y1,z1,x2,y2,z2) (x1)/16.0f,(y1)/16.0f,(z1)/16.0f, (x2)/16.0f,(y2)/16.0f,(z2)/16.0f +#define BoxDesc_Rot(x, y, z) (x)/16.0f,(y)/16.0f,(z)/16.0f +#define BoxDesc_Box(x1,y1,z1,x2,y2,z2) BoxDesc_Dims(x1,y1,z1,x2,y2,z2), BoxDesc_Bounds(x1,y1,z1,x2,y2,z2) + +/* Builds a box model assuming the follow texture layout: +let SW = sides width, BW = body width, BH = body height +********************************************************************************************* +|----------SW----------|----------BW----------|----------BW----------|----------------------| +|S--------------------S|S--------top---------S|S-------bottom-------S|----------------------| +|W--------------------W|W--------tex---------W|W--------tex---------W|----------------------| +|----------SW----------|----------BW----------|----------BW----------|----------------------| +********************************************************************************************* +|----------SW----------|----------BW----------|----------SW----------|----------BW----------| +|B--------left--------B|B-------front--------B|B-------right--------B|B--------back--------B| +|H--------tex---------H|H--------tex---------H|H--------tex---------H|H--------tex---------H| +|----------SW----------|----------BW----------|----------SW----------|----------BW----------| +********************************************************************************************* */ +CC_API void BoxDesc_BuildBox(struct ModelPart* part, const struct BoxDesc* desc); + +/* Builds a box model assuming the follow texture layout: +let SW = sides width, BW = body width, BH = body height +********************************************************************************************* +|----------SW----------|----------BW----------|----------BW----------|----------------------| +|S--------------------S|S-------front--------S|S--------back--------S|----------------------| +|W--------------------W|W--------tex---------W|W--------tex---------W|----------------------| +|----------SW----------|----------BW----------|----------BW----------|----------------------| +********************************************************************************************* +|----------SW----------|----------BW----------|----------BW----------|----------SW----------| +|B--------left--------B|B-------bottom-------B|B-------right--------B|B--------top---------B| +|H--------tex---------H|H--------tex---------H|H--------tex---------H|H--------tex---------H| +|----------SW----------|----------BW----------|----------BW----------|----------------------| +********************************************************************************************* */ +CC_API void BoxDesc_BuildRotatedBox(struct ModelPart* part, const struct BoxDesc* desc); + +/* DEPRECATED */ +CC_API void BoxDesc_XQuad(struct Model* m, int texX, int texY, int texWidth, int texHeight, float z1, float z2, float y1, float y2, float x, cc_bool swapU); +CC_API void BoxDesc_YQuad(struct Model* m, int texX, int texY, int texWidth, int texHeight, float x1, float x2, float z1, float z2, float y, cc_bool swapU); +CC_API void BoxDesc_ZQuad(struct Model* m, int texX, int texY, int texWidth, int texHeight, float x1, float x2, float y1, float y2, float z, cc_bool swapU); + +CC_API void BoxDesc_XQuad2(struct Model* m, float z1, float z2, float y1, float y2, float x, int u1, int v1, int u2, int v2); +CC_API void BoxDesc_YQuad2(struct Model* m, float x1, float x2, float z1, float z2, float y, int u1, int v1, int u2, int v2); +CC_API void BoxDesc_ZQuad2(struct Model* m, float x1, float x2, float y1, float y2, float z, int u1, int v1, int u2, int v2); + +/* CustomModels */ + +#define MAX_CUSTOM_MODELS 64 +#define MAX_CUSTOM_MODEL_PARTS 64 +#define MAX_CUSTOM_MODEL_ANIMS 4 + +enum CustomModelAnimType { + CustomModelAnimType_None = 0, + CustomModelAnimType_Head = 1, + CustomModelAnimType_LeftLegX = 2, + CustomModelAnimType_RightLegX = 3, + CustomModelAnimType_LeftArmX = 4, + CustomModelAnimType_LeftArmZ = 5, + CustomModelAnimType_RightArmX = 6, + CustomModelAnimType_RightArmZ = 7, + CustomModelAnimType_Spin = 8, + CustomModelAnimType_SpinVelocity = 9, + CustomModelAnimType_SinRotate = 10, + CustomModelAnimType_SinRotateVelocity = 11, + CustomModelAnimType_SinTranslate = 12, + CustomModelAnimType_SinTranslateVelocity = 13, + CustomModelAnimType_SinSize = 14, + CustomModelAnimType_SinSizeVelocity = 15, + CustomModelAnimType_FlipRotate = 16, + CustomModelAnimType_FlipRotateVelocity = 17, + CustomModelAnimType_FlipTranslate = 18, + CustomModelAnimType_FlipTranslateVelocity = 19, + CustomModelAnimType_FlipSize = 20, + CustomModelAnimType_FlipSizeVelocity = 21 +}; + +enum CustomModelAnimAxis { + CustomModelAnimAxis_X = 0, + CustomModelAnimAxis_Y = 1, + CustomModelAnimAxis_Z = 2, +}; + +struct CustomModelAnim { + cc_uint8 type; + cc_uint8 axis; + float a, b, c, d; +}; + +struct CustomModelPartDef { + Vec3 min, max; + /* uv coords in order: top, bottom, front, back, left, right */ + cc_uint16 u1[6], v1[6], u2[6], v2[6]; + Vec3 rotationOrigin; + cc_uint8 flags; +}; + +struct CustomModelPart { + struct ModelPart modelPart; + Vec3 rotation; /* rotation angles */ + struct CustomModelAnim anims[MAX_CUSTOM_MODEL_ANIMS]; + cc_bool fullbright; + cc_bool firstPersonArm; +}; + +struct CustomModel { + struct Model model; + char name[STRING_SIZE + 1]; + cc_bool registered, defined; + cc_uint8 curPartIndex; + + float nameY; + float eyeY; + Vec3 collisionBounds; + struct AABB pickingBoundsAABB; + + cc_uint16 uScale; + cc_uint16 vScale; + + cc_uint8 numParts; + cc_uint8 numArmParts; + struct CustomModelPart parts[MAX_CUSTOM_MODEL_PARTS]; +}; + +struct CustomModel* CustomModel_Get(int id); +void CustomModel_BuildPart(struct CustomModel* cm, struct CustomModelPartDef* part); +void CustomModel_Register(struct CustomModel* cm); +void CustomModel_Undefine(struct CustomModel* cm); + +#endif diff --git a/src/Options.c b/src/Options.c new file mode 100644 index 0000000..d571a75 --- /dev/null +++ b/src/Options.c @@ -0,0 +1,227 @@ +#include "Options.h" +#include "String.h" +#include "ExtMath.h" +#include "Platform.h" +#include "Stream.h" +#include "Errors.h" +#include "Utils.h" +#include "Logger.h" +#include "PackedCol.h" + +struct StringsBuffer Options; +static struct StringsBuffer changedOpts; +cc_result Options_LoadResult; +static cc_bool savingPaused; +#if defined CC_BUILD_WEB || defined CC_BUILD_MOBILE || defined CC_BUILD_CONSOLE + #define OPTIONS_SAVE_IMMEDIATELY +#endif + +void Options_Free(void) { + StringsBuffer_Clear(&Options); + StringsBuffer_Clear(&changedOpts); +} + +static cc_bool HasChanged(const cc_string* key) { + cc_string entry; + int i; + + for (i = 0; i < changedOpts.count; i++) { + entry = StringsBuffer_UNSAFE_Get(&changedOpts, i); + if (String_CaselessEquals(&entry, key)) return true; + } + return false; +} + +static cc_bool Options_LoadFilter(const cc_string* entry) { + cc_string key, value; + String_UNSAFE_Separate(entry, '=', &key, &value); + return !HasChanged(&key); +} + +void Options_Load(void) { + /* Increase from max 512 to 2048 per entry */ + StringsBuffer_SetLengthBits(&Options, 11); + Options_LoadResult = EntryList_Load(&Options, "options-default.txt", '=', NULL); + Options_LoadResult = EntryList_Load(&Options, "options.txt", '=', NULL); +} + +void Options_Reload(void) { + cc_string entry, key, value; + int i; + + /* Reset all the unchanged options */ + for (i = Options.count - 1; i >= 0; i--) { + entry = StringsBuffer_UNSAFE_Get(&Options, i); + String_UNSAFE_Separate(&entry, '=', &key, &value); + + if (HasChanged(&key)) continue; + StringsBuffer_Remove(&Options, i); + } + /* Load only options which have not changed */ + Options_LoadResult = EntryList_Load(&Options, "options.txt", '=', Options_LoadFilter); +} + +static void SaveOptions(void) { + EntryList_Save(&Options, "options.txt"); + StringsBuffer_Clear(&changedOpts); +} + +void Options_SaveIfChanged(void) { + if (!changedOpts.count) return; + + Options_Reload(); + SaveOptions(); +} + +void Options_PauseSaving(void) { savingPaused = true; } + +void Options_ResumeSaving(void) { + savingPaused = false; +#if defined OPTIONS_SAVE_IMMEDIATELY + SaveOptions(); +#endif +} + + +cc_bool Options_UNSAFE_Get(const char* keyRaw, cc_string* value) { + int idx; + cc_string key = String_FromReadonly(keyRaw); + + *value = EntryList_UNSAFE_Get(&Options, &key, '='); + if (value->length) return true; + + /* Fallback to without '-' (e.g. "hacks-fly" to "fly") */ + /* Needed for some very old options.txt files */ + idx = String_IndexOf(&key, '-'); + if (idx == -1) return false; + key = String_UNSAFE_SubstringAt(&key, idx + 1); + + *value = EntryList_UNSAFE_Get(&Options, &key, '='); + return value->length > 0; +} + +void Options_Get(const char* key, cc_string* value, const char* defValue) { + cc_string str; + Options_UNSAFE_Get(key, &str); + value->length = 0; + + if (str.length) { + String_AppendString(value, &str); + } else { + String_AppendConst(value, defValue); + } +} + +int Options_GetInt(const char* key, int min, int max, int defValue) { + cc_string str; + int value; + if (!Options_UNSAFE_Get(key, &str)) return defValue; + if (!Convert_ParseInt(&str, &value)) return defValue; + + Math_Clamp(value, min, max); + return value; +} + +cc_bool Options_GetBool(const char* key, cc_bool defValue) { + cc_string str; + cc_bool value; + if (!Options_UNSAFE_Get(key, &str)) return defValue; + if (!Convert_ParseBool(&str, &value)) return defValue; + + return value; +} + +float Options_GetFloat(const char* key, float min, float max, float defValue) { + cc_string str; + float value; + if (!Options_UNSAFE_Get(key, &str)) return defValue; + if (!Convert_ParseFloat(&str, &value)) return defValue; + + Math_Clamp(value, min, max); + return value; +} + +int Options_GetEnum(const char* key, int defValue, const char* const* names, int namesCount) { + cc_string str; + if (!Options_UNSAFE_Get(key, &str)) return defValue; + return Utils_ParseEnum(&str, defValue, names, namesCount); +} + +cc_bool Options_GetColor(const char* key, cc_uint8* rgb) { + cc_string value, parts[3]; + if (!Options_UNSAFE_Get(key, &value)) return false; + if (PackedCol_TryParseHex(&value, rgb)) return true; + + /* Try parsing as R,G,B instead */ + return String_UNSAFE_Split(&value, ',', parts, 3) + && Convert_ParseUInt8(&parts[0], &rgb[0]) + && Convert_ParseUInt8(&parts[1], &rgb[1]) + && Convert_ParseUInt8(&parts[2], &rgb[2]); +} + + +void Options_SetBool(const char* keyRaw, cc_bool value) { + static const cc_string str_true = String_FromConst("True"); + static const cc_string str_false = String_FromConst("False"); + Options_Set(keyRaw, value ? &str_true : &str_false); +} + +void Options_SetInt(const char* keyRaw, int value) { + cc_string str; char strBuffer[STRING_INT_CHARS]; + String_InitArray(str, strBuffer); + String_AppendInt(&str, value); + Options_Set(keyRaw, &str); +} + +void Options_Set(const char* keyRaw, const cc_string* value) { + cc_string key = String_FromReadonly(keyRaw); + Options_SetString(&key, value); +} + +void Options_SetString(const cc_string* key, const cc_string* value) { + if (!value || !value->length) { + if (!EntryList_Remove(&Options, key, '=')) return; + } else { + EntryList_Set(&Options, key, value, '='); + } + +#if defined OPTIONS_SAVE_IMMEDIATELY + if (!savingPaused) SaveOptions(); +#endif + + if (HasChanged(key)) return; + StringsBuffer_Add(&changedOpts, key); +} + +void Options_SetSecure(const char* opt, const cc_string* src) { + char data[2000], encData[1500+1]; + cc_string tmp, enc; + cc_result res; + if (!src->length) return; + + String_InitArray(enc, encData); + res = Platform_Encrypt(src->buffer, src->length, &enc); + if (res) { Platform_Log2("Error %e encrypting option %c", &res, opt); return; } + + /* base64 encode the data, as user might edit options.txt with a text editor */ + if (enc.length > 1500) Logger_Abort("too large to base64"); + tmp.buffer = data; + tmp.length = Convert_ToBase64(enc.buffer, enc.length, data); + tmp.capacity = tmp.length; + Options_Set(opt, &tmp); +} + +void Options_GetSecure(const char* opt, cc_string* dst) { + cc_uint8 data[1500]; + int dataLen; + cc_string raw; + cc_result res; + + Options_UNSAFE_Get(opt, &raw); + if (!raw.length) return; + if (raw.length > 2000) Logger_Abort("too large to base64"); + + dataLen = Convert_FromBase64(raw.buffer, raw.length, data); + res = Platform_Decrypt(data, dataLen, dst); + if (res) Platform_Log2("Error %e decrypting option %c", &res, opt); +} diff --git a/src/Options.h b/src/Options.h new file mode 100644 index 0000000..e359b3d --- /dev/null +++ b/src/Options.h @@ -0,0 +1,158 @@ +#ifndef CC_OPTIONS_H +#define CC_OPTIONS_H +#include "Core.h" +/* +Manages loading and saving options +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +#define OPT_MUSIC_VOLUME "musicvolume" +#define OPT_SOUND_VOLUME "soundsvolume" +#define OPT_FORCE_OPENAL "forceopenal" +#define OPT_MIN_MUSIC_DELAY "music-mindelay" +#define OPT_MAX_MUSIC_DELAY "music-maxdelay" + +#define OPT_VIEW_DISTANCE "viewdist" +#define OPT_BLOCK_PHYSICS "singleplayerphysics" +#define OPT_NAMES_MODE "namesmode" +#define OPT_INVERT_MOUSE "invertmouse" +#define OPT_SENSITIVITY "mousesensitivity" +#define OPT_FPS_LIMIT "fpslimit" +#define OPT_DEFAULT_TEX_PACK "defaulttexpack" +#define OPT_VIEW_BOBBING "viewbobbing" +#define OPT_ENTITY_SHADOW "entityshadow" +#define OPT_RENDER_TYPE "normal" +#define OPT_SMOOTH_LIGHTING "gfx-smoothlighting" +#define OPT_LIGHTING_MODE "gfx-lightingmode" +#define OPT_MIPMAPS "gfx-mipmaps" +#define OPT_CHAT_LOGGING "chat-logging" +#define OPT_WINDOW_WIDTH "window-width" +#define OPT_WINDOW_HEIGHT "window-height" + +#define OPT_HACKS_ENABLED "hacks-hacksenabled" +#define OPT_FIELD_OF_VIEW "hacks-fov" +#define OPT_SPEED_FACTOR "hacks-speedmultiplier" +#define OPT_JUMP_VELOCITY "hacks-jumpvelocity" +#define OPT_MODIFIABLE_LIQUIDS "hacks-liquidsbreakable" +#define OPT_PUSHBACK_PLACING "hacks-pushbackplacing" +#define OPT_NOCLIP_SLIDE "hacks-noclipslide" +#define OPT_CAMERA_CLIPPING "hacks-cameraclipping" +#define OPT_WOM_STYLE_HACKS "hacks-womstylehacks" +#define OPT_FULL_BLOCK_STEP "hacks-fullblockstep" +#define OPT_HACK_PERM_MSGS "hacks-perm-msgs" + +#define OPT_TAB_AUTOCOMPLETE "gui-tab-autocomplete" +#define OPT_SHOW_BLOCK_IN_HAND "gui-blockinhand" +#define OPT_CHATLINES "gui-chatlines" +#define OPT_CLICKABLE_CHAT "gui-chatclickable" +#define OPT_USE_CHAT_FONT "gui-arialchatfont" +#define OPT_HOTBAR_SCALE "gui-hotbarscale" +#define OPT_INVENTORY_SCALE "gui-inventoryscale" +#define OPT_CHAT_SCALE "gui-chatscale" +#define OPT_CHAT_AUTO_SCALE "gui-autoscalechat" +#define OPT_CROSSHAIR_SCALE "gui-crosshairscale" +#define OPT_SHOW_FPS "gui-showfps" +#define OPT_FONT_NAME "gui-fontname" +#define OPT_BLACK_TEXT "gui-blacktextshadows" + +#define OPT_LANDSCAPE_MODE "landscape-mode" +#define OPT_CLASSIC_MODE "mode-classic" +#define OPT_CUSTOM_BLOCKS "nostalgia-customblocks" +#define OPT_CPE "nostalgia-usecpe" +#define OPT_SERVER_TEXTURES "nostalgia-servertextures" +#define OPT_CLASSIC_GUI "nostalgia-classicgui" +#define OPT_SIMPLE_ARMS_ANIM "nostalgia-simplearms" +#define OPT_CLASSIC_TABLIST "nostalgia-classictablist" +#define OPT_CLASSIC_OPTIONS "nostalgia-classicoptions" +#define OPT_CLASSIC_HACKS "nostalgia-hacks" +#define OPT_CLASSIC_ARM_MODEL "nostalgia-classicarm" +#define OPT_CLASSIC_CHAT "nostalgia-classicchat" +#define OPT_CLASSIC_INVENTORY "nostalgia-classicinventory" +#define OPT_MAX_CHUNK_UPDATES "gfx-maxchunkupdates" +#define OPT_CAMERA_MASS "cameramass" +#define OPT_CAMERA_SMOOTH "camera-smooth" +#define OPT_GRAB_CURSOR "win-grab-cursor" +#define OPT_TOUCH_BUTTONS "gui-touchbuttons" +#define OPT_TOUCH_HALIGN "gui-touch-halign" +#define OPT_TOUCH_SCALE "gui-touchscale" +#define OPT_HTTP_ONLY "http-no-https" +#define OPT_HTTPS_VERIFY "https-verify" +#define OPT_SKIN_SERVER "http-skinserver" +#define OPT_RAW_INPUT "win-raw-input" +#define OPT_DPI_SCALING "win-dpi-scaling" +#define OPT_GAME_VERSION "game-version" +#define OPT_INV_SCROLLBAR_SCALE "inv-scrollbar-scale" +#define OPT_ANAGLYPH3D "anaglyph-3d" + +#define OPT_SELECTED_BLOCK_OUTLINE_COLOR "selected-block-outline-color" +#define OPT_SELECTED_BLOCK_OUTLINE_OPACITY "selected-block-outline-opacity" +#define OPT_SELECTED_BLOCK_OUTLINE_SCALE "selected-block-outline-scale" + +#define LOPT_SESSION "launcher-session" +#define LOPT_USERNAME "launcher-cc-username" +#define LOPT_PASSWORD "launcher-cc-password" + +#define LOPT_AUTO_CLOSE "autocloselauncher" +#define LOPT_SHOW_EMPTY "launcher-show-empty" + +#define ROPT_SERVER "launcher-server" +#define ROPT_USER "launcher-username" +#define ROPT_IP "launcher-ip" +#define ROPT_PORT "launcher-port" +#define ROPT_MPPASS "launcher-mppass" + +#define SOPT_SERVICES "server-services" + +struct StringsBuffer; +extern struct StringsBuffer Options; +extern cc_result Options_LoadResult; +/* Frees any memory allocated in storing options. */ +void Options_Free(void); + +/* Loads options from disc. */ +void Options_Load(void); +/* Reloads options from disc, leaving options changed in this session alone. */ +CC_API void Options_Reload(void); +/* Saves options to disc, if any were changed via Options_SetXYZ since last save. */ +CC_API void Options_SaveIfChanged(void); +/* Temporarily prevents saving options */ +/* NOTE: Only makes a difference on some platforms */ +void Options_PauseSaving(void); +/* Enables saving options again */ +/* NOTE: Only makes a difference on some platforms */ +void Options_ResumeSaving(void); + +/* Sets value to value of option directly in Options.Buffer if found, String_Empty if not. */ +/* Returns whether the option was actually found. */ +STRING_REF cc_bool Options_UNSAFE_Get(const char* keyRaw, cc_string* value); +/* Returns value of given option, or default value if not found. */ +CC_API void Options_Get(const char* key, cc_string* value, const char* defValue); +/* Returns value of given option as an integer, or default value if could not be converted. */ +CC_API int Options_GetInt(const char* key, int min, int max, int defValue); +/* Returns value of given option as a bool, or default value if could not be converted. */ +CC_API cc_bool Options_GetBool(const char* key, cc_bool defValue); +/* Returns value of given option as a float, or default value if could not be converted. */ +CC_API float Options_GetFloat(const char* key, float min, float max, float defValue); +/* Returns value of given option as an integer, or default value if could not be converted. */ +/* NOTE: Conversion is done by going through all elements of names, returning index of a match. */ +CC_API int Options_GetEnum(const char* key, int defValue, const char* const* names, int namesCount); +/* Attempts to parse the value of the given option into an RGB (3 byte) colour. */ +/* Returns whether the option was actually found and could be parsed into a colour. */ +cc_bool Options_GetColor(const char* key, cc_uint8* rgb); + +/* Sets value of given option to either "true" or "false". */ +CC_API void Options_SetBool(const char* keyRaw, cc_bool value); +/* Sets value of given option to given integer converted to a string. */ +CC_API void Options_SetInt(const char* keyRaw, int value); +/* Sets value of given option to given string. */ +CC_API void Options_Set(const char* keyRaw, const cc_string* value); +/* Sets value of given option to given string. */ +CC_API void Options_SetString(const cc_string* key, const cc_string* value); + +/* Attempts to securely encode an option. */ +/* NOTE: Not all platforms support secure saving. */ +void Options_SetSecure(const char* opt, const cc_string* data); +/* Attempts to securely decode an option. */ +/* NOTE: Not all platforms support secure saving. */ +void Options_GetSecure(const char* opt, cc_string* data); +#endif diff --git a/src/PackedCol.c b/src/PackedCol.c new file mode 100644 index 0000000..465f480 --- /dev/null +++ b/src/PackedCol.c @@ -0,0 +1,95 @@ +#include "PackedCol.h" +#include "String.h" +#include "ExtMath.h" + +PackedCol PackedCol_Scale(PackedCol a, float t) { + cc_uint8 R = (cc_uint8)(PackedCol_R(a) * t); + cc_uint8 G = (cc_uint8)(PackedCol_G(a) * t); + cc_uint8 B = (cc_uint8)(PackedCol_B(a) * t); + return (a & PACKEDCOL_A_MASK) | PackedCol_R_Bits(R) | PackedCol_G_Bits(G) | PackedCol_B_Bits(B); +} + +PackedCol PackedCol_Lerp(PackedCol a, PackedCol b, float t) { + cc_uint8 R = (cc_uint8)Math_Lerp(PackedCol_R(a), PackedCol_R(b), t); + cc_uint8 G = (cc_uint8)Math_Lerp(PackedCol_G(a), PackedCol_G(b), t); + cc_uint8 B = (cc_uint8)Math_Lerp(PackedCol_B(a), PackedCol_B(b), t); + return (a & PACKEDCOL_A_MASK) | PackedCol_R_Bits(R) | PackedCol_G_Bits(G) | PackedCol_B_Bits(B); +} + +PackedCol PackedCol_Tint(PackedCol a, PackedCol b) { + cc_uint32 R = PackedCol_R(a) * PackedCol_R(b) / 255; + cc_uint32 G = PackedCol_G(a) * PackedCol_G(b) / 255; + cc_uint32 B = PackedCol_B(a) * PackedCol_B(b) / 255; + /* TODO: don't shift when multiplying */ + return (a & PACKEDCOL_A_MASK) | (R << PACKEDCOL_R_SHIFT) | (G << PACKEDCOL_G_SHIFT) | (B << PACKEDCOL_B_SHIFT); +} + +PackedCol PackedCol_ScreenBlend(PackedCol a, PackedCol b) { + PackedCol finalColor, aInverted, bInverted; + cc_uint8 R, G, B; + /* With Screen blend mode, the values of the pixels in the two layers are inverted, multiplied, and then inverted again. */ + R = 255 - PackedCol_R(a); + G = 255 - PackedCol_G(a); + B = 255 - PackedCol_B(a); + aInverted = PackedCol_Make(R, G, B, 255); + + R = 255 - PackedCol_R(b); + G = 255 - PackedCol_G(b); + B = 255 - PackedCol_B(b); + bInverted = PackedCol_Make(R, G, B, 255); + + finalColor = PackedCol_Tint(aInverted, bInverted); + R = 255 - PackedCol_R(finalColor); + G = 255 - PackedCol_G(finalColor); + B = 255 - PackedCol_B(finalColor); + return PackedCol_Make(R, G, B, 255); +} + +void PackedCol_GetShaded(PackedCol normal, PackedCol* xSide, PackedCol* zSide, PackedCol* yMin) { + *xSide = PackedCol_Scale(normal, PACKEDCOL_SHADE_X); + *zSide = PackedCol_Scale(normal, PACKEDCOL_SHADE_Z); + *yMin = PackedCol_Scale(normal, PACKEDCOL_SHADE_YMIN); +} + +int PackedCol_DeHex(char hex) { + if (hex >= '0' && hex <= '9') { + return (hex - '0'); + } else if (hex >= 'a' && hex <= 'f') { + return (hex - 'a') + 10; + } else if (hex >= 'A' && hex <= 'F') { + return (hex - 'A') + 10; + } + return -1; +} + +cc_bool PackedCol_Unhex(const char* src, int* dst, int count) { + int i; + for (i = 0; i < count; i++) { + dst[i] = PackedCol_DeHex(src[i]); + if (dst[i] == -1) return false; + } + return true; +} + +void PackedCol_ToHex(cc_string* str, PackedCol value) { + String_AppendHex(str, PackedCol_R(value)); + String_AppendHex(str, PackedCol_G(value)); + String_AppendHex(str, PackedCol_B(value)); +} + +cc_bool PackedCol_TryParseHex(const cc_string* str, cc_uint8* rgb) { + int bits[6]; + char* buffer = str->buffer; + + /* accept XXYYZZ or #XXYYZZ forms */ + if (str->length < 6) return false; + if (str->length > 6 && (str->buffer[0] != '#' || str->length > 7)) return false; + + if (buffer[0] == '#') buffer++; + if (!PackedCol_Unhex(buffer, bits, 6)) return false; + + rgb[0] = (cc_uint8)((bits[0] << 4) | bits[1]); + rgb[1] = (cc_uint8)((bits[2] << 4) | bits[3]); + rgb[2] = (cc_uint8)((bits[4] << 4) | bits[5]); + return true; +} diff --git a/src/PackedCol.h b/src/PackedCol.h new file mode 100644 index 0000000..498be40 --- /dev/null +++ b/src/PackedCol.h @@ -0,0 +1,64 @@ +#ifndef CC_PACKEDCOL_H +#define CC_PACKEDCOL_H +#include "Core.h" +/* Manipulates a packed 32 bit RGBA colour, in a format suitable for the native 3D graphics API. + Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +typedef cc_uint32 PackedCol; +#if (CC_GFX_BACKEND == CC_GFX_BACKEND_D3D9) || defined CC_BUILD_XBOX || defined CC_BUILD_DREAMCAST + #define PACKEDCOL_B_SHIFT 0 + #define PACKEDCOL_G_SHIFT 8 + #define PACKEDCOL_R_SHIFT 16 + #define PACKEDCOL_A_SHIFT 24 +#elif defined CC_BIG_ENDIAN + #define PACKEDCOL_R_SHIFT 24 + #define PACKEDCOL_G_SHIFT 16 + #define PACKEDCOL_B_SHIFT 8 + #define PACKEDCOL_A_SHIFT 0 +#else + #define PACKEDCOL_R_SHIFT 0 + #define PACKEDCOL_G_SHIFT 8 + #define PACKEDCOL_B_SHIFT 16 + #define PACKEDCOL_A_SHIFT 24 +#endif + +#define PACKEDCOL_R_MASK (0xFFU << PACKEDCOL_R_SHIFT) +#define PACKEDCOL_G_MASK (0xFFU << PACKEDCOL_G_SHIFT) +#define PACKEDCOL_B_MASK (0xFFU << PACKEDCOL_B_SHIFT) +#define PACKEDCOL_A_MASK (0xFFU << PACKEDCOL_A_SHIFT) + +#define PackedCol_R(col) ((cc_uint8)(col >> PACKEDCOL_R_SHIFT)) +#define PackedCol_G(col) ((cc_uint8)(col >> PACKEDCOL_G_SHIFT)) +#define PackedCol_B(col) ((cc_uint8)(col >> PACKEDCOL_B_SHIFT)) +#define PackedCol_A(col) ((cc_uint8)(col >> PACKEDCOL_A_SHIFT)) + +#define PackedCol_R_Bits(col) ((cc_uint8)(col) << PACKEDCOL_R_SHIFT) +#define PackedCol_G_Bits(col) ((cc_uint8)(col) << PACKEDCOL_G_SHIFT) +#define PackedCol_B_Bits(col) ((cc_uint8)(col) << PACKEDCOL_B_SHIFT) +#define PackedCol_A_Bits(col) ((cc_uint8)(col) << PACKEDCOL_A_SHIFT) + +#define PackedCol_Make(r, g, b, a) (PackedCol_R_Bits(r) | PackedCol_G_Bits(g) | PackedCol_B_Bits(b) | PackedCol_A_Bits(a)) +#define PACKEDCOL_WHITE PackedCol_Make(255, 255, 255, 255) +#define PACKEDCOL_RGB_MASK (PACKEDCOL_R_MASK | PACKEDCOL_G_MASK | PACKEDCOL_B_MASK) + +/* Scales RGB components of the given colour. */ +CC_API PackedCol PackedCol_Scale(PackedCol value, float t); +/* Linearly interpolates RGB components of the two given colours. */ +CC_API PackedCol PackedCol_Lerp(PackedCol a, PackedCol b, float t); +/* Multiplies RGB components of the two given colours. */ +CC_API PackedCol PackedCol_Tint(PackedCol a, PackedCol b); +/* Adds the two colors together in a way that gives a brighter result. */ +CC_API PackedCol PackedCol_ScreenBlend(PackedCol a, PackedCol b); + +CC_NOINLINE int PackedCol_DeHex(char hex); +CC_NOINLINE cc_bool PackedCol_Unhex(const char* src, int* dst, int count); +CC_NOINLINE void PackedCol_ToHex(cc_string* str, PackedCol value); +CC_NOINLINE cc_bool PackedCol_TryParseHex(const cc_string* str, cc_uint8* rgb); + +#define PACKEDCOL_SHADE_X 0.6f +#define PACKEDCOL_SHADE_Z 0.8f +#define PACKEDCOL_SHADE_YMIN 0.5f +/* Retrieves shaded colours for ambient block face lighting */ +void PackedCol_GetShaded(PackedCol normal, PackedCol* xSide, PackedCol* zSide, PackedCol* yMin); +#endif diff --git a/src/Particle.c b/src/Particle.c new file mode 100644 index 0000000..977fbfa --- /dev/null +++ b/src/Particle.c @@ -0,0 +1,619 @@ +#include "Particle.h" +#include "Block.h" +#include "World.h" +#include "ExtMath.h" +#include "Lighting.h" +#include "Entity.h" +#include "TexturePack.h" +#include "Graphics.h" +#include "Funcs.h" +#include "Game.h" +#include "Event.h" + + +/*########################################################################################################################* +*------------------------------------------------------Particle base------------------------------------------------------* +*#########################################################################################################################*/ +static GfxResourceID particles_TexId, particles_VB; +#define PARTICLES_MAX 600 +static RNGState rnd; +static cc_bool hitTerrain; +typedef cc_bool (*CanPassThroughFunc)(BlockID b); + +void Particle_DoRender(const Vec2* size, const Vec3* pos, const TextureRec* rec, PackedCol col, struct VertexTextured* v) { + struct Matrix* view; + float sX, sY; + Vec3 centre; + float aX, aY, aZ, bX, bY, bZ; + + sX = size->x * 0.5f; sY = size->y * 0.5f; + centre = *pos; centre.y += sY; + view = &Gfx.View; + + aX = view->row1.x * sX; aY = view->row2.x * sX; aZ = view->row3.x * sX; /* right * size.x * 0.5f */ + bX = view->row1.y * sY; bY = view->row2.y * sY; bZ = view->row3.y * sY; /* up * size.y * 0.5f */ + + v->x = centre.x - aX - bX; v->y = centre.y - aY - bY; v->z = centre.z - aZ - bZ; v->Col = col; v->U = rec->u1; v->V = rec->v2; v++; + v->x = centre.x - aX + bX; v->y = centre.y - aY + bY; v->z = centre.z - aZ + bZ; v->Col = col; v->U = rec->u1; v->V = rec->v1; v++; + v->x = centre.x + aX + bX; v->y = centre.y + aY + bY; v->z = centre.z + aZ + bZ; v->Col = col; v->U = rec->u2; v->V = rec->v1; v++; + v->x = centre.x + aX - bX; v->y = centre.y + aY - bY; v->z = centre.z + aZ - bZ; v->Col = col; v->U = rec->u2; v->V = rec->v2; v++; +} + +static cc_bool CollidesHor(Vec3* nextPos, BlockID block) { + Vec3 horPos = Vec3_Create3((float)Math_Floor(nextPos->x), 0.0f, (float)Math_Floor(nextPos->z)); + Vec3 min, max; + Vec3_Add(&min, &Blocks.MinBB[block], &horPos); + Vec3_Add(&max, &Blocks.MaxBB[block], &horPos); + return nextPos->x >= min.x && nextPos->z >= min.z && nextPos->x < max.x && nextPos->z < max.z; +} + +static BlockID GetBlock(int x, int y, int z) { + if (World_Contains(x, y, z)) return World_GetBlock(x, y, z); + + if (y >= Env.EdgeHeight) return BLOCK_AIR; + if (y >= Env_SidesHeight) return Env.EdgeBlock; + return Env.SidesBlock; +} + +static cc_bool ClipY(struct Particle* p, int y, cc_bool topFace, CanPassThroughFunc canPassThrough) { + BlockID block; + Vec3 minBB, maxBB; + float collideY; + cc_bool collideVer; + + if (y < 0) { + p->nextPos.y = ENTITY_ADJUSTMENT; + p->lastPos.y = ENTITY_ADJUSTMENT; + + Vec3_Set(p->velocity, 0,0,0); + hitTerrain = true; + return false; + } + + block = GetBlock((int)p->nextPos.x, y, (int)p->nextPos.z); + if (canPassThrough(block)) return true; + minBB = Blocks.MinBB[block]; maxBB = Blocks.MaxBB[block]; + + collideY = y + (topFace ? maxBB.y : minBB.y); + collideVer = topFace ? (p->nextPos.y < collideY) : (p->nextPos.y > collideY); + + if (collideVer && CollidesHor(&p->nextPos, block)) { + float adjust = topFace ? ENTITY_ADJUSTMENT : -ENTITY_ADJUSTMENT; + p->lastPos.y = collideY + adjust; + p->nextPos.y = p->lastPos.y; + + Vec3_Set(p->velocity, 0,0,0); + hitTerrain = true; + return false; + } + return true; +} + +static cc_bool IntersectsBlock(struct Particle* p, CanPassThroughFunc canPassThrough) { + BlockID cur = GetBlock((int)p->nextPos.x, (int)p->nextPos.y, (int)p->nextPos.z); + float minY = Math_Floor(p->nextPos.y) + Blocks.MinBB[cur].y; + float maxY = Math_Floor(p->nextPos.y) + Blocks.MaxBB[cur].y; + + return !canPassThrough(cur) && p->nextPos.y >= minY && p->nextPos.y < maxY && CollidesHor(&p->nextPos, cur); +} + +static cc_bool PhysicsTick(struct Particle* p, float gravity, CanPassThroughFunc canPassThrough, float delta) { + Vec3 velocity; + int y, begY, endY; + + p->lastPos = p->nextPos; + if (IntersectsBlock(p, canPassThrough)) return true; + + p->velocity.y -= gravity * delta; + begY = Math_Floor(p->nextPos.y); + + Vec3_Mul1(&velocity, &p->velocity, delta * 3.0f); + Vec3_Add(&p->nextPos, &p->nextPos, &velocity); + endY = Math_Floor(p->nextPos.y); + + if (p->velocity.y > 0.0f) { + /* don't test block we are already in */ + for (y = begY + 1; y <= endY && ClipY(p, y, false, canPassThrough); y++) {} + } else { + for (y = begY; y >= endY && ClipY(p, y, true, canPassThrough); y--) {} + } + + p->lifetime -= delta; + return p->lifetime < 0.0f; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Rain particle-----------------------------------------------------* +*#########################################################################################################################*/ +static struct Particle rain_Particles[PARTICLES_MAX]; +static int rain_count; +static TextureRec rain_rec = { 2.0f/128.0f, 14.0f/128.0f, 5.0f/128.0f, 16.0f/128.0f }; + +static cc_bool RainParticle_CanPass(BlockID block) { + cc_uint8 draw = Blocks.Draw[block]; + return draw == DRAW_GAS || draw == DRAW_SPRITE; +} + +static cc_bool RainParticle_Tick(struct Particle* p, float delta) { + hitTerrain = false; + return PhysicsTick(p, 3.5f, RainParticle_CanPass, delta) || hitTerrain; +} + +static void RainParticle_Render(struct Particle* p, float t, struct VertexTextured* vertices) { + Vec3 pos; + Vec2 size; + PackedCol col; + int x, y, z; + + Vec3_Lerp(&pos, &p->lastPos, &p->nextPos, t); + size.x = p->size * 0.015625f; size.y = size.x; + + x = Math_Floor(pos.x); y = Math_Floor(pos.y); z = Math_Floor(pos.z); + col = Lighting.Color(x, y, z); + Particle_DoRender(&size, &pos, &rain_rec, col, vertices); +} + +static void Rain_Render(float t) { + struct VertexTextured* data; + int i; + if (!rain_count) return; + + data = (struct VertexTextured*)Gfx_LockDynamicVb(particles_VB, + VERTEX_FORMAT_TEXTURED, rain_count * 4); + for (i = 0; i < rain_count; i++) { + RainParticle_Render(&rain_Particles[i], t, data); + data += 4; + } + + Gfx_BindTexture(particles_TexId); + Gfx_UnlockDynamicVb(particles_VB); + Gfx_DrawVb_IndexedTris(rain_count * 4); +} + +static void Rain_RemoveAt(int i) { + for (; i < rain_count - 1; i++) { + rain_Particles[i] = rain_Particles[i + 1]; + } + rain_count--; +} + +static void Rain_Tick(float delta) { + int i; + for (i = 0; i < rain_count; i++) { + if (RainParticle_Tick(&rain_Particles[i], delta)) { + Rain_RemoveAt(i); i--; + } + } +} + +void Particles_RainSnowEffect(float x, float y, float z) { + struct Particle* p; + int i, type; + + for (i = 0; i < 2; i++) { + if (rain_count == PARTICLES_MAX) Rain_RemoveAt(0); + p = &rain_Particles[rain_count++]; + + p->velocity.x = Random_Float(&rnd) * 0.8f - 0.4f; /* [-0.4, 0.4] */ + p->velocity.z = Random_Float(&rnd) * 0.8f - 0.4f; + p->velocity.y = Random_Float(&rnd) + 0.4f; + + p->lastPos.x = x + Random_Float(&rnd); /* [0.0, 1.0] */ + p->lastPos.y = y + Random_Float(&rnd) * 0.1f + 0.01f; + p->lastPos.z = z + Random_Float(&rnd); + + p->nextPos = p->lastPos; + p->lifetime = 40.0f; + + type = Random_Next(&rnd, 30); + p->size = type >= 28 ? 2 : (type >= 25 ? 4 : 3); + } +} + + +/*########################################################################################################################* +*------------------------------------------------------Terrain particle---------------------------------------------------* +*#########################################################################################################################*/ +struct TerrainParticle { + struct Particle base; + TextureRec rec; + TextureLoc texLoc; + BlockID block; +}; + +static struct TerrainParticle terrain_particles[PARTICLES_MAX]; +static int terrain_count; +static cc_uint16 terrain_1DCount[ATLAS1D_MAX_ATLASES]; +static cc_uint16 terrain_1DIndices[ATLAS1D_MAX_ATLASES]; + +static cc_bool TerrainParticle_CanPass(BlockID block) { + cc_uint8 draw = Blocks.Draw[block]; + return draw == DRAW_GAS || draw == DRAW_SPRITE || Blocks.IsLiquid[block]; +} + +static cc_bool TerrainParticle_Tick(struct TerrainParticle* p, float delta) { + return PhysicsTick(&p->base, Blocks.ParticleGravity[p->block], TerrainParticle_CanPass, delta); +} + +static void TerrainParticle_Render(struct TerrainParticle* p, float t, struct VertexTextured* vertices) { + PackedCol col = PACKEDCOL_WHITE; + Vec3 pos; + Vec2 size; + int x, y, z; + + Vec3_Lerp(&pos, &p->base.lastPos, &p->base.nextPos, t); + size.x = p->base.size * 0.015625f; size.y = size.x; + + if (!Blocks.Brightness[p->block]) { + x = Math_Floor(pos.x); y = Math_Floor(pos.y); z = Math_Floor(pos.z); + col = Lighting.Color_XSide(x, y, z); + } + + Block_Tint(col, p->block); + Particle_DoRender(&size, &pos, &p->rec, col, vertices); +} + +static void Terrain_Update1DCounts(void) { + int i, index; + + for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) { + terrain_1DCount[i] = 0; + terrain_1DIndices[i] = 0; + } + for (i = 0; i < terrain_count; i++) { + index = Atlas1D_Index(terrain_particles[i].texLoc); + terrain_1DCount[index] += 4; + } + for (i = 1; i < Atlas1D.Count; i++) { + terrain_1DIndices[i] = terrain_1DIndices[i - 1] + terrain_1DCount[i - 1]; + } +} + +static void Terrain_Render(float t) { + struct VertexTextured* data; + struct VertexTextured* ptr; + int offset = 0; + int i, index; + if (!terrain_count) return; + + data = (struct VertexTextured*)Gfx_LockDynamicVb(particles_VB, + VERTEX_FORMAT_TEXTURED, terrain_count * 4); + Terrain_Update1DCounts(); + for (i = 0; i < terrain_count; i++) { + index = Atlas1D_Index(terrain_particles[i].texLoc); + ptr = data + terrain_1DIndices[index]; + + TerrainParticle_Render(&terrain_particles[i], t, ptr); + terrain_1DIndices[index] += 4; + } + + Gfx_UnlockDynamicVb(particles_VB); + for (i = 0; i < Atlas1D.Count; i++) { + int partCount = terrain_1DCount[i]; + if (!partCount) continue; + + Gfx_BindTexture(Atlas1D.TexIds[i]); + Gfx_DrawVb_IndexedTris_Range(partCount, offset); + offset += partCount; + } +} + +static void Terrain_RemoveAt(int i) { + for (; i < terrain_count - 1; i++) { + terrain_particles[i] = terrain_particles[i + 1]; + } + terrain_count--; +} + +static void Terrain_Tick(float delta) { + int i; + for (i = 0; i < terrain_count; i++) { + if (TerrainParticle_Tick(&terrain_particles[i], delta)) { + Terrain_RemoveAt(i); i--; + } + } +} + +void Particles_BreakBlockEffect(IVec3 coords, BlockID old, BlockID now) { + struct TerrainParticle* p; + TextureLoc loc; + int texIndex; + TextureRec baseRec, rec; + Vec3 origin, minBB, maxBB; + + /* texture UV variables */ + float uScale, vScale, maxU2, maxV2; + int minX, minZ, maxX, maxZ; + int minU, minV, maxU, maxV; + int maxUsedU, maxUsedV; + + /* per-particle variables */ + float cellX, cellY, cellZ; + Vec3 cell; + int x, y, z, type; + + if (now != BLOCK_AIR || Blocks.Draw[old] == DRAW_GAS) return; + IVec3_ToVec3(&origin, &coords); + loc = Block_Tex(old, FACE_XMIN); + + baseRec = Atlas1D_TexRec(loc, 1, &texIndex); + uScale = (1.0f/16.0f); vScale = (1.0f/16.0f) * Atlas1D.InvTileSize; + + minBB = Blocks.MinBB[old]; maxBB = Blocks.MaxBB[old]; + minX = (int)(minBB.x * 16); maxX = (int)(maxBB.x * 16); + minZ = (int)(minBB.z * 16); maxZ = (int)(maxBB.z * 16); + + minU = min(minX, minZ); minV = (int)(16 - maxBB.y * 16); + maxU = min(maxX, maxZ); maxV = (int)(16 - minBB.y * 16); + /* This way we can avoid creating particles which outside the bounds and need to be clamped */ + maxUsedU = maxU; maxUsedV = maxV; + if (minU < 12 && maxU > 12) maxUsedU = 12; + if (minV < 12 && maxV > 12) maxUsedV = 12; + + #define GRID_SIZE 4 + /* gridOffset gives the centre of the cell on a grid */ + #define CELL_CENTRE ((1.0f / GRID_SIZE) * 0.5f) + + maxU2 = baseRec.u1 + maxU * uScale; + maxV2 = baseRec.v1 + maxV * vScale; + for (x = 0; x < GRID_SIZE; x++) { + for (y = 0; y < GRID_SIZE; y++) { + for (z = 0; z < GRID_SIZE; z++) { + + cellX = (float)x / GRID_SIZE; cellY = (float)y / GRID_SIZE; cellZ = (float)z / GRID_SIZE; + cell = Vec3_Create3(CELL_CENTRE + cellX, CELL_CENTRE / 2 + cellY, CELL_CENTRE + cellZ); + if (cell.x < minBB.x || cell.x > maxBB.x || cell.y < minBB.y + || cell.y > maxBB.y || cell.z < minBB.z || cell.z > maxBB.z) continue; + + if (terrain_count == PARTICLES_MAX) Terrain_RemoveAt(0); + p = &terrain_particles[terrain_count++]; + + /* centre random offset around [-0.2, 0.2] */ + p->base.velocity.x = CELL_CENTRE + (cellX - 0.5f) + (Random_Float(&rnd) * 0.4f - 0.2f); + p->base.velocity.y = CELL_CENTRE + (cellY - 0.0f) + (Random_Float(&rnd) * 0.4f - 0.2f); + p->base.velocity.z = CELL_CENTRE + (cellZ - 0.5f) + (Random_Float(&rnd) * 0.4f - 0.2f); + + rec = baseRec; + rec.u1 = baseRec.u1 + Random_Range(&rnd, minU, maxUsedU) * uScale; + rec.v1 = baseRec.v1 + Random_Range(&rnd, minV, maxUsedV) * vScale; + rec.u2 = rec.u1 + 4 * uScale; + rec.v2 = rec.v1 + 4 * vScale; + rec.u2 = min(rec.u2, maxU2) - 0.01f * uScale; + rec.v2 = min(rec.v2, maxV2) - 0.01f * vScale; + + Vec3_Add(&p->base.lastPos, &origin, &cell); + p->base.nextPos = p->base.lastPos; + p->base.lifetime = 0.3f + Random_Float(&rnd) * 1.2f; + + p->rec = rec; + p->texLoc = loc; + p->block = old; + type = Random_Next(&rnd, 30); + p->base.size = type >= 28 ? 12 : (type >= 25 ? 10 : 8); + } + } + } +} + + +/*########################################################################################################################* +*-------------------------------------------------------Custom particle---------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_NETWORKING +struct CustomParticle { + struct Particle base; + int effectId; + float totalLifespan; +}; + +struct CustomParticleEffect Particles_CustomEffects[256]; +static struct CustomParticle custom_particles[PARTICLES_MAX]; +static int custom_count; +static cc_uint8 collideFlags; +#define EXPIRES_UPON_TOUCHING_GROUND (1 << 0) +#define SOLID_COLLIDES (1 << 1) +#define LIQUID_COLLIDES (1 << 2) +#define LEAF_COLLIDES (1 << 3) + +static cc_bool CustomParticle_CanPass(BlockID block) { + cc_uint8 draw, collide; + + draw = Blocks.Draw[block]; + if (draw == DRAW_TRANSPARENT_THICK && !(collideFlags & LEAF_COLLIDES)) return true; + + collide = Blocks.Collide[block]; + if (collide == COLLIDE_SOLID && (collideFlags & SOLID_COLLIDES)) return false; + if (collide == COLLIDE_LIQUID && (collideFlags & LIQUID_COLLIDES)) return false; + return true; +} + +static cc_bool CustomParticle_Tick(struct CustomParticle* p, float delta) { + struct CustomParticleEffect* e = &Particles_CustomEffects[p->effectId]; + hitTerrain = false; + collideFlags = e->collideFlags; + + return PhysicsTick(&p->base, e->gravity, CustomParticle_CanPass, delta) + || (hitTerrain && (e->collideFlags & EXPIRES_UPON_TOUCHING_GROUND)); +} + +static void CustomParticle_Render(struct CustomParticle* p, float t, struct VertexTextured* vertices) { + struct CustomParticleEffect* e = &Particles_CustomEffects[p->effectId]; + Vec3 pos; + Vec2 size; + PackedCol col; + TextureRec rec = e->rec; + int x, y, z; + + float time_lived = p->totalLifespan - p->base.lifetime; + int curFrame = Math_Floor(e->frameCount * (time_lived / p->totalLifespan)); + float shiftU = curFrame * (rec.u2 - rec.u1); + + rec.u1 += shiftU;/* * 0.0078125f; */ + rec.u2 += shiftU;/* * 0.0078125f; */ + + Vec3_Lerp(&pos, &p->base.lastPos, &p->base.nextPos, t); + size.x = p->base.size; size.y = size.x; + + x = Math_Floor(pos.x); y = Math_Floor(pos.y); z = Math_Floor(pos.z); + col = e->fullBright ? PACKEDCOL_WHITE : Lighting.Color(x, y, z); + col = PackedCol_Tint(col, e->tintCol); + + Particle_DoRender(&size, &pos, &rec, col, vertices); +} + +static void Custom_Render(float t) { + struct VertexTextured* data; + int i; + if (!custom_count) return; + + data = (struct VertexTextured*)Gfx_LockDynamicVb(particles_VB, + VERTEX_FORMAT_TEXTURED, custom_count * 4); + for (i = 0; i < custom_count; i++) { + CustomParticle_Render(&custom_particles[i], t, data); + data += 4; + } + + Gfx_BindTexture(particles_TexId); + Gfx_UnlockDynamicVb(particles_VB); + Gfx_DrawVb_IndexedTris(custom_count * 4); +} + +static void Custom_RemoveAt(int i) { + for (; i < custom_count - 1; i++) { + custom_particles[i] = custom_particles[i + 1]; + } + custom_count--; +} + +static void Custom_Tick(float delta) { + int i; + for (i = 0; i < custom_count; i++) { + if (CustomParticle_Tick(&custom_particles[i], delta)) { + Custom_RemoveAt(i); i--; + } + } +} + +void Particles_CustomEffect(int effectID, float x, float y, float z, float originX, float originY, float originZ) { + struct CustomParticle* p; + struct CustomParticleEffect* e = &Particles_CustomEffects[effectID]; + int i, count = e->particleCount; + Vec3 offset, delta; + float d; + + for (i = 0; i < count; i++) { + if (custom_count == PARTICLES_MAX) Custom_RemoveAt(0); + p = &custom_particles[custom_count++]; + p->effectId = effectID; + + offset.x = Random_Float(&rnd) - 0.5f; + offset.y = Random_Float(&rnd) - 0.5f; + offset.z = Random_Float(&rnd) - 0.5f; + Vec3_Normalise(&offset); + + /* See https://karthikkaranth.me/blog/generating-random-points-in-a-sphere/ */ + /* 'Using normally distributed random numbers' */ + d = Random_Float(&rnd); + d = Math_Exp2(Math_Log2(d) / 3.0); /* d^1/3 for better distribution */ + d *= e->spread; + + p->base.lastPos.x = x + offset.x * d; + p->base.lastPos.y = y + offset.y * d; + p->base.lastPos.z = z + offset.z * d; + + Vec3 origin = { originX, originY, originZ }; + Vec3_Sub(&delta, &p->base.lastPos, &origin); + Vec3_Normalise(&delta); + + p->base.velocity.x = delta.x * e->speed; + p->base.velocity.y = delta.y * e->speed; + p->base.velocity.z = delta.z * e->speed; + + p->base.nextPos = p->base.lastPos; + p->base.lifetime = e->baseLifetime + (e->baseLifetime * e->lifetimeVariation) * ((Random_Float(&rnd) - 0.5f) * 2); + p->totalLifespan = p->base.lifetime; + + p->base.size = e->size + (e->size * e->sizeVariation) * ((Random_Float(&rnd) - 0.5f) * 2); + + /* Don't spawn custom particle inside a block (otherwise it appears */ + /* for a few frames, then disappears in first PhysicsTick call)*/ + collideFlags = e->collideFlags; + if (IntersectsBlock(&p->base, CustomParticle_CanPass)) custom_count--; + } +} +#else +static int custom_count; + +static void Custom_Render(float t) { } +static void Custom_Tick(float delta) { } +#endif + + +/*########################################################################################################################* +*--------------------------------------------------------Particles--------------------------------------------------------* +*#########################################################################################################################*/ +void Particles_Render(float t) { + if (!terrain_count && !rain_count && !custom_count) return; + + if (Gfx.LostContext) return; + if (!particles_VB) + particles_VB = Gfx_CreateDynamicVb(VERTEX_FORMAT_TEXTURED, PARTICLES_MAX * 4); + + Gfx_SetAlphaTest(true); + + Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED); + Terrain_Render(t); + Rain_Render(t); + Custom_Render(t); + + Gfx_SetAlphaTest(false); +} + +static void Particles_Tick(struct ScheduledTask* task) { + float delta = task->interval; + Terrain_Tick(delta); + Rain_Tick(delta); + Custom_Tick(delta); +} + + +/*########################################################################################################################* +*---------------------------------------------------Particles component---------------------------------------------------* +*#########################################################################################################################*/ +static void ParticlesPngProcess(struct Stream* stream, const cc_string* name) { + Game_UpdateTexture(&particles_TexId, stream, name, NULL, NULL); +} +static struct TextureEntry particles_entry = { "particles.png", ParticlesPngProcess }; + + +static void OnContextLost(void* obj) { + Gfx_DeleteDynamicVb(&particles_VB); + + if (Gfx.ManagedTextures) return; + Gfx_DeleteTexture(&particles_TexId); +} + +static void OnBreakBlockEffect_Handler(void* obj, IVec3 coords, BlockID old, BlockID now) { + Particles_BreakBlockEffect(coords, old, now); +} + +static void OnInit(void) { + ScheduledTask_Add(GAME_DEF_TICKS, Particles_Tick); + Random_SeedFromCurrentTime(&rnd); + TextureEntry_Register(&particles_entry); + + Event_Register_(&UserEvents.BlockChanged, NULL, OnBreakBlockEffect_Handler); + Event_Register_(&GfxEvents.ContextLost, NULL, OnContextLost); +} + +static void OnFree(void) { OnContextLost(NULL); } + +static void OnReset(void) { rain_count = 0; terrain_count = 0; custom_count = 0; } + +struct IGameComponent Particles_Component = { + OnInit, /* Init */ + OnFree, /* Free */ + OnReset, /* Reset */ + OnReset /* OnNewMap */ +}; diff --git a/src/Particle.h b/src/Particle.h new file mode 100644 index 0000000..f322bbc --- /dev/null +++ b/src/Particle.h @@ -0,0 +1,46 @@ +#ifndef CC_PARTICLE_H +#define CC_PARTICLE_H +#include "Vectors.h" +#include "PackedCol.h" +/* +Represents particle effects, and manages rendering and spawning particles +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +struct IGameComponent; +struct VertexTextured; +struct ScheduledTask; +extern struct IGameComponent Particles_Component; + +struct Particle { + Vec3 velocity; + float lifetime; + Vec3 lastPos, nextPos; + float size; +}; + +struct CustomParticleEffect { + TextureRec rec; + PackedCol tintCol; + cc_uint8 frameCount; + cc_uint8 particleCount; + cc_uint8 collideFlags; + cc_bool fullBright; + float size; + float sizeVariation; + float spread; /* how far from the spawnpoint their location can vary */ + float speed; /* how fast they move away/towards the origin */ + float gravity; + float baseLifetime; /* how long (in seconds) the particle lives for */ + float lifetimeVariation; +}; + +extern struct CustomParticleEffect Particles_CustomEffects[256]; + +/* http://www.opengl-tutorial.org/intermediate-tutorials/billboards-particles/billboards/ */ +void Particle_DoRender(const Vec2* size, const Vec3* pos, const TextureRec* rec, PackedCol col, struct VertexTextured* vertices); +void Particles_Render(float t); +void Particles_BreakBlockEffect(IVec3 coords, BlockID oldBlock, BlockID block); +void Particles_RainSnowEffect(float x, float y, float z); +void Particles_CustomEffect(int effectID, float x, float y, float z, float originX, float originY, float originZ); +#endif diff --git a/src/Physics.c b/src/Physics.c new file mode 100644 index 0000000..361ee65 --- /dev/null +++ b/src/Physics.c @@ -0,0 +1,244 @@ +#include "Physics.h" +#include "ExtMath.h" +#include "Block.h" +#include "World.h" +#include "Platform.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Logger.h" +#include "Entity.h" + + +/*########################################################################################################################* +*----------------------------------------------------------AABB-----------------------------------------------------------* +*#########################################################################################################################*/ +void AABB_Make(struct AABB* result, const Vec3* pos, const Vec3* size) { + result->Min.x = pos->x - size->x * 0.5f; + result->Min.y = pos->y; + result->Min.z = pos->z - size->z * 0.5f; + + result->Max.x = pos->x + size->x * 0.5f; + result->Max.y = pos->y + size->y; + result->Max.z = pos->z + size->z * 0.5f; +} + +void AABB_Offset(struct AABB* result, const struct AABB* bb, const Vec3* amount) { + Vec3_Add(&result->Min, &bb->Min, amount); + Vec3_Add(&result->Max, &bb->Max, amount); +} + +cc_bool AABB_Intersects(const struct AABB* bb, const struct AABB* other) { + return + bb->Max.x >= other->Min.x && bb->Min.x <= other->Max.x && + bb->Max.y >= other->Min.y && bb->Min.y <= other->Max.y && + bb->Max.z >= other->Min.z && bb->Min.z <= other->Max.z; +} + +cc_bool AABB_Contains(const struct AABB* parent, const struct AABB* child) { + return + child->Min.x >= parent->Min.x && child->Min.y >= parent->Min.y && child->Min.z >= parent->Min.z && + child->Max.x <= parent->Max.x && child->Max.y <= parent->Max.y && child->Max.z <= parent->Max.z; +} + +cc_bool AABB_ContainsPoint(const struct AABB* parent, const Vec3* P) { + return + P->x >= parent->Min.x && P->y >= parent->Min.y && P->z >= parent->Min.z && + P->x <= parent->Max.x && P->y <= parent->Max.y && P->z <= parent->Max.z; +} + + +/*########################################################################################################################* +*------------------------------------------------------Intersection-------------------------------------------------------* +*#########################################################################################################################*/ +static Vec3 Intersection_InverseRotate(Vec3 pos, struct Entity* target) { + pos = Vec3_RotateY(pos, -target->RotY * MATH_DEG2RAD); + pos = Vec3_RotateZ(pos, -target->RotZ * MATH_DEG2RAD); + pos = Vec3_RotateX(pos, -target->RotX * MATH_DEG2RAD); + return pos; +} + +cc_bool Intersection_RayIntersectsRotatedBox(Vec3 origin, Vec3 dir, struct Entity* target, float* tMin, float* tMax) { + Vec3 delta, invDir; + struct AABB bb; + + /* This is the rotated AABB of the model we want to test for intersection + * + / \ we then perform a counter *---* and we can then do + ====>* x * rotation to the rotated AABB | x | a standard AABB test + \ / and ray origin and direction *---* with the rotated ray + * / + / + */ + Vec3_Sub(&delta, &origin, &target->Position); /* delta = origin - target.Position */ + delta = Intersection_InverseRotate(delta, target); /* delta = UndoRotation(delta) */ + Vec3_Add(&origin, &delta, &target->Position); /* origin = delta + target.Position */ + + dir = Intersection_InverseRotate(dir, target); + Entity_GetPickingBounds(target, &bb); + + invDir.x = 1.0f / dir.x; + invDir.y = 1.0f / dir.y; + invDir.z = 1.0f / dir.z; + return Intersection_RayIntersectsBox(origin, invDir, bb.Min, bb.Max, tMin, tMax); +} + +cc_bool Intersection_RayIntersectsBox(Vec3 origin, Vec3 invDir, Vec3 min, Vec3 max, float* t0, float* t1) { + float tmin, tmax, tymin, tymax, tzmin, tzmax; + *t0 = 0; *t1 = 0; + + if (invDir.x >= 0) { + tmin = (min.x - origin.x) * invDir.x; + tmax = (max.x - origin.x) * invDir.x; + } else { + tmin = (max.x - origin.x) * invDir.x; + tmax = (min.x - origin.x) * invDir.x; + } + + if (invDir.y >= 0) { + tymin = (min.y - origin.y) * invDir.y; + tymax = (max.y - origin.y) * invDir.y; + } else { + tymin = (max.y - origin.y) * invDir.y; + tymax = (min.y - origin.y) * invDir.y; + } + + if (tmin > tymax || tymin > tmax) return false; + if (tymin > tmin) tmin = tymin; + if (tymax < tmax) tmax = tymax; + + if (invDir.z >= 0) { + tzmin = (min.z - origin.z) * invDir.z; + tzmax = (max.z - origin.z) * invDir.z; + } else { + tzmin = (max.z - origin.z) * invDir.z; + tzmax = (min.z - origin.z) * invDir.z; + } + + if (tmin > tzmax || tzmin > tmax) return false; + if (tzmin > tmin) tmin = tzmin; + if (tzmax < tmax) tmax = tzmax; + + *t0 = tmin; *t1 = tmax; + return tmin >= 0.0f; +} + + +/*########################################################################################################################* +*----------------------------------------------------Collisions finder----------------------------------------------------* +*#########################################################################################################################*/ +#define SEARCHER_STATES_MIN 64 +static struct SearcherState searcherDefaultStates[SEARCHER_STATES_MIN]; +static cc_uint32 searcherCapacity = SEARCHER_STATES_MIN; +struct SearcherState* Searcher_States = searcherDefaultStates; + +static void Searcher_QuickSort(int left, int right) { + struct SearcherState* keys = Searcher_States; struct SearcherState key; + + while (left < right) { + int i = left, j = right; + float pivot = keys[(i + j) >> 1].tSquared; + + /* partition the list */ + while (i <= j) { + while (pivot > keys[i].tSquared) i++; + while (pivot < keys[j].tSquared) j--; + QuickSort_Swap_Maybe(); + } + /* recurse into the smaller subset */ + QuickSort_Recurse(Searcher_QuickSort); + } +} + +int Searcher_FindReachableBlocks(struct Entity* entity, struct AABB* entityBB, struct AABB* entityExtentBB) { + Vec3 vel = entity->Velocity; + IVec3 min, max; + cc_uint32 elements; + struct SearcherState* curState; + int count; + + BlockID block; + struct AABB blockBB; + float xx, yy, zz, tx, ty, tz; + int x, y, z; + + Entity_GetBounds(entity, entityBB); + /* Exact maximum extent the entity can reach, and the equivalent map coordinates. */ + entityExtentBB->Min.x = entityBB->Min.x + (vel.x < 0.0f ? vel.x : 0.0f); + entityExtentBB->Min.y = entityBB->Min.y + (vel.y < 0.0f ? vel.y : 0.0f); + entityExtentBB->Min.z = entityBB->Min.z + (vel.z < 0.0f ? vel.z : 0.0f); + + entityExtentBB->Max.x = entityBB->Max.x + (vel.x > 0.0f ? vel.x : 0.0f); + entityExtentBB->Max.y = entityBB->Max.y + (vel.y > 0.0f ? vel.y : 0.0f); + entityExtentBB->Max.z = entityBB->Max.z + (vel.z > 0.0f ? vel.z : 0.0f); + + IVec3_Floor(&min, &entityExtentBB->Min); + IVec3_Floor(&max, &entityExtentBB->Max); + elements = (max.x - min.x + 1) * (max.y - min.y + 1) * (max.z - min.z + 1); + + if (elements > searcherCapacity) { + Searcher_Free(); + searcherCapacity = elements; + Searcher_States = (struct SearcherState*)Mem_Alloc(elements, sizeof(struct SearcherState), "collision search states"); + } + curState = Searcher_States; + + /* Order loops so that we minimise cache misses */ + for (y = min.y; y <= max.y; y++) { + for (z = min.z; z <= max.z; z++) { + for (x = min.x; x <= max.x; x++) { + block = World_GetPhysicsBlock(x, y, z); + if (Blocks.Collide[block] != COLLIDE_SOLID) continue; + + xx = (float)x; yy = (float)y; zz = (float)z; + blockBB.Min = Blocks.MinBB[block]; + blockBB.Min.x += xx; blockBB.Min.y += yy; blockBB.Min.z += zz; + blockBB.Max = Blocks.MaxBB[block]; + blockBB.Max.x += xx; blockBB.Max.y += yy; blockBB.Max.z += zz; + + if (!AABB_Intersects(entityExtentBB, &blockBB)) continue; /* necessary for non whole blocks. (slabs) */ + Searcher_CalcTime(&vel, entityBB, &blockBB, &tx, &ty, &tz); + if (tx > 1.0f || ty > 1.0f || tz > 1.0f) continue; + + curState->x = (x << 3) | (block & 0x007); + curState->y = (y << 4) | ((block & 0x078) >> 3); + curState->z = (z << 3) | ((block & 0x380) >> 7); + curState->tSquared = tx * tx + ty * ty + tz * tz; + curState++; + } + } + } + + count = (int)(curState - Searcher_States); + if (count) Searcher_QuickSort(0, count - 1); + return count; +} + +void Searcher_CalcTime(Vec3* vel, struct AABB *entityBB, struct AABB* blockBB, float* tx, float* ty, float* tz) { + float dx = vel->x > 0.0f ? blockBB->Min.x - entityBB->Max.x : entityBB->Min.x - blockBB->Max.x; + float dy = vel->y > 0.0f ? blockBB->Min.y - entityBB->Max.y : entityBB->Min.y - blockBB->Max.y; + float dz = vel->z > 0.0f ? blockBB->Min.z - entityBB->Max.z : entityBB->Min.z - blockBB->Max.z; + + if (entityBB->Max.x >= blockBB->Min.x && entityBB->Min.x <= blockBB->Max.x) { + *tx = 0.0f; /* Inlined XIntersects test */ + } else { + *tx = vel->x == 0.0f ? MATH_LARGENUM : Math_AbsF(dx / vel->x); + } + + if (entityBB->Max.y >= blockBB->Min.y && entityBB->Min.y <= blockBB->Max.y) { + *ty = 0.0f; /* Inlined YIntersects test */ + } else { + *ty = vel->y == 0.0f ? MATH_LARGENUM : Math_AbsF(dy / vel->y); + } + + if (entityBB->Max.z >= blockBB->Min.z && entityBB->Min.z <= blockBB->Max.z) { + *tz = 0.0f; /* Inlined ZIntersects test */ + } else { + *tz = vel->z == 0.0f ? MATH_LARGENUM : Math_AbsF(dz / vel->z); + } +} + +void Searcher_Free(void) { + if (Searcher_States != searcherDefaultStates) Mem_Free(Searcher_States); + Searcher_States = searcherDefaultStates; + searcherCapacity = SEARCHER_STATES_MIN; +} diff --git a/src/Physics.h b/src/Physics.h new file mode 100644 index 0000000..c39cdaf --- /dev/null +++ b/src/Physics.h @@ -0,0 +1,41 @@ +#ifndef CC_PHYSICS_H +#define CC_PHYSICS_H +#include "Vectors.h" +/* +Provides various physics related structs and methods such as: + - An axis aligned bounding box, and various methods related to them. + - Various methods for intersecting geometry. + - Calculates all possible blocks that a moving entity can intersect with +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct Entity; + +/* Descibes an axis aligned bounding box. */ +struct AABB { Vec3 Min, Max; }; + +/* Makes an AABB centred at the base of the given position, that is: */ +/* Min = [pos.x - size.x/2, pos.y , pos.z - size.z/2] */ +/* Max = [pos.x + size.x/2, pos.y + size.y, pos.z + size.z/2] */ +CC_API void AABB_Make(struct AABB* result, const Vec3* pos, const Vec3* size); +/* Adds amount to Min and Max corners of the given AABB. */ +void AABB_Offset(struct AABB* result, const struct AABB* bb, const Vec3* amount); +/* Whether the given AABB touches another AABB on any axis. */ +cc_bool AABB_Intersects(const struct AABB* bb, const struct AABB* other); +/* Whether the given ABBB contains another AABB. */ +cc_bool AABB_Contains(const struct AABB* parent, const struct AABB* child); +/* Whether the given point lies insides bounds of the given AABB. */ +cc_bool AABB_ContainsPoint(const struct AABB* parent, const Vec3* P); + +/* Calculates the intersection point of a ray and a rotated bounding box. */ +cc_bool Intersection_RayIntersectsRotatedBox(Vec3 origin, Vec3 dir, struct Entity* target, float* tMin, float* tMax); +/* Calculates the intersection point of a ray and a bounding box. +Source: http://www.cs.utah.edu/~awilliam/box/box.pdf */ +/* NOTE: invDir is inverse of ray's direction (i.e. 1.0f / dir) */ +cc_bool Intersection_RayIntersectsBox(Vec3 origin, Vec3 invDir, Vec3 min, Vec3 max, float* t0, float* t1); + +struct SearcherState { int x, y, z; float tSquared; }; +extern struct SearcherState* Searcher_States; +int Searcher_FindReachableBlocks(struct Entity* entity, struct AABB* entityBB, struct AABB* entityExtentBB); +void Searcher_CalcTime(Vec3* vel, struct AABB *entityBB, struct AABB* blockBB, float* tx, float* ty, float* tz); +void Searcher_Free(void); +#endif diff --git a/src/Picking.c b/src/Picking.c new file mode 100644 index 0000000..f6b96f1 --- /dev/null +++ b/src/Picking.c @@ -0,0 +1,261 @@ +#include "Picking.h" +#include "ExtMath.h" +#include "Game.h" +#include "Physics.h" +#include "Entity.h" +#include "World.h" +#include "Funcs.h" +#include "Block.h" +#include "Logger.h" +#include "Camera.h" + +static float pickedPos_dist; +static void TestAxis(struct RayTracer* t, float dAxis, Face fAxis) { + dAxis = Math_AbsF(dAxis); + if (dAxis >= pickedPos_dist) return; + + pickedPos_dist = dAxis; + t->closest = fAxis; +} + +static void SetAsValid(struct RayTracer* t) { + t->translatedPos = t->pos; + t->valid = true; + + pickedPos_dist = MATH_LARGENUM; + TestAxis(t, t->intersect.x - t->Min.x, FACE_XMIN); + TestAxis(t, t->intersect.x - t->Max.x, FACE_XMAX); + TestAxis(t, t->intersect.y - t->Min.y, FACE_YMIN); + TestAxis(t, t->intersect.y - t->Max.y, FACE_YMAX); + TestAxis(t, t->intersect.z - t->Min.z, FACE_ZMIN); + TestAxis(t, t->intersect.z - t->Max.z, FACE_ZMAX); + + switch (t->closest) { + case FACE_XMIN: t->translatedPos.x--; break; + case FACE_XMAX: t->translatedPos.x++; break; + case FACE_ZMIN: t->translatedPos.z--; break; + case FACE_ZMAX: t->translatedPos.z++; break; + case FACE_YMIN: t->translatedPos.y--; break; + case FACE_YMAX: t->translatedPos.y++; break; + } +} + +void RayTracer_SetInvalid(struct RayTracer* t) { + static const IVec3 pos = { -1, -1, -1 }; + t->pos = pos; + t->translatedPos = pos; + + t->valid = false; + t->block = BLOCK_AIR; + t->closest = FACE_COUNT; +} + +static float RayTracer_Div(float a, float b) { + if (Math_AbsF(b) < 0.000001f) return MATH_LARGENUM; + return a / b; +} + +void RayTracer_Init(struct RayTracer* t, const Vec3* origin, const Vec3* dir) { + IVec3 cellBoundary; + t->origin = *origin; t->dir = *dir; + + t->invDir.x = RayTracer_Div(1.0f, dir->x); + t->invDir.y = RayTracer_Div(1.0f, dir->y); + t->invDir.z = RayTracer_Div(1.0f, dir->z); + + /* Rounds the position's X, Y and Z down to the nearest integer values. */ + /* The cell in which the ray starts. */ + IVec3_Floor(&t->pos, origin); + /* Determine which way we go. */ + t->step.x = Math_Sign(dir->x); t->step.y = Math_Sign(dir->y); t->step.z = Math_Sign(dir->z); + + /* Calculate cell boundaries. When the step (i.e. direction sign) is positive, + the next boundary is AFTER our current position, meaning that we have to add 1. + Otherwise, it is BEFORE our current position, in which case we add nothing. */ + cellBoundary.x = t->pos.x + (t->step.x > 0 ? 1 : 0); + cellBoundary.y = t->pos.y + (t->step.y > 0 ? 1 : 0); + cellBoundary.z = t->pos.z + (t->step.z > 0 ? 1 : 0); + + /* NOTE: we want it so if dir.x = 0, tmax.x = positive infinity + Determine how far we can travel along the ray before we hit a voxel boundary. */ + t->tMax.x = RayTracer_Div(cellBoundary.x - origin->x, dir->x); /* Boundary is a plane on the YZ axis. */ + t->tMax.y = RayTracer_Div(cellBoundary.y - origin->y, dir->y); /* Boundary is a plane on the XZ axis. */ + t->tMax.z = RayTracer_Div(cellBoundary.z - origin->z, dir->z); /* Boundary is a plane on the XY axis. */ + + /* Determine how far we must travel along the ray before we have crossed a gridcell. */ + t->tDelta.x = (float)t->step.x * t->invDir.x; + t->tDelta.y = (float)t->step.y * t->invDir.y; + t->tDelta.z = (float)t->step.z * t->invDir.z; +} + +void RayTracer_Step(struct RayTracer* t) { + /* For each step, determine which distance to the next voxel boundary is lowest + (i.e. which voxel boundary is nearest) and walk that way. */ + if (t->tMax.x < t->tMax.y && t->tMax.x < t->tMax.z) { + /* tMax.x is the lowest, an YZ cell boundary plane is nearest. */ + t->pos.x += t->step.x; + t->tMax.x += t->tDelta.x; + } else if (t->tMax.y < t->tMax.z) { + /* tMax.y is the lowest, an XZ cell boundary plane is nearest. */ + t->pos.y += t->step.y; + t->tMax.y += t->tDelta.y; + } else { + /* tMax.z is the lowest, an XY cell boundary plane is nearest. */ + t->pos.z += t->step.z; + t->tMax.z += t->tDelta.z; + } +} + +#define BORDER BLOCK_BEDROCK +typedef cc_bool (*IntersectTest)(struct RayTracer* t); + +static BlockID Picking_GetInside(int x, int y, int z) { + int floorY; + + if (World_ContainsXZ(x, z)) { + if (y >= World.Height) return BLOCK_AIR; + if (y >= 0) return World_GetBlock(x, y, z); + floorY = 0; + } else { + floorY = Env_SidesHeight; + } + + /* bedrock on bottom or outside map */ + return Env.SidesBlock != BLOCK_AIR && y < floorY ? BORDER : BLOCK_AIR; +} + +static BlockID Picking_GetOutside(int x, int y, int z, IVec3 origin) { + cc_bool sides = Env.SidesBlock != BLOCK_AIR; + if (World_ContainsXZ(x, z)) { + if (y >= World.Height) return BLOCK_AIR; + + if (sides && y == -1 && origin.y > 0) return BORDER; + if (sides && y == 0 && origin.y < 0) return BORDER; + + if (sides && y >= 0 && y < Env_SidesHeight && origin.y < Env_SidesHeight) { + if (x == 0 && origin.x < 0) return BORDER; + if (z == 0 && origin.z < 0) return BORDER; + if (x == World.MaxX && origin.x >= 0) return BORDER; + if (z == World.MaxZ && origin.z >= 0) return BORDER; + } + if (y >= 0) return World_GetBlock(x, y, z); + + } else if (Env.SidesBlock != BLOCK_AIR && y >= 0 && y < Env_SidesHeight) { + /* | + X|\ If # represents player and is above the map border, + | \ they should be able to place blocks on other map borders + *--\---- (i.e. same behaviour as when player is inside map) + # + */ + if (x == -1 && origin.x >= 0 && z >= 0 && z < World.Length) return BORDER; + if (x == World.Width && origin.x < 0 && z >= 0 && z < World.Length) return BORDER; + if (z == -1 && origin.z >= 0 && x >= 0 && x < World.Width ) return BORDER; + if (z == World.Length && origin.z < 0 && x >= 0 && x < World.Width ) return BORDER; + } + return BLOCK_AIR; +} + +static cc_bool RayTrace(struct RayTracer* t, const Vec3* origin, const Vec3* dir, float reach, IntersectTest intersect) { + IVec3 pOrigin; + cc_bool insideMap; + float reachSq; + Vec3 v; + + float dxMin, dxMax, dx; + float dyMin, dyMax, dy; + float dzMin, dzMax, dz; + int i, x, y, z; + + RayTracer_Init(t, origin, dir); + /* Check if origin is at NaN (happens if player's position is at infinity) */ + if (origin->x != origin->x || origin->y != origin->y || origin->z != origin->z) return false; + + IVec3_Floor(&pOrigin, origin); + /* This used to be World_Contains(pOrigin.x, pOrigin.y, pOrigin.z), however */ + /* this caused a bug when you were above the map (but still inside the map */ + /* horizontally) - if borders height was > map height, you would wrongly */ + /* pick blocks on the INSIDE of the map borders instead of OUTSIDE them */ + insideMap = World_ContainsXZ(pOrigin.x, pOrigin.z) && pOrigin.y >= 0; + reachSq = reach * reach; + + for (i = 0; i < 25000; i++) { + x = t->pos.x; y = t->pos.y; z = t->pos.z; + v.x = (float)x; v.y = (float)y; v.z = (float)z; + + t->block = insideMap ? Picking_GetInside(x, y, z) : Picking_GetOutside(x, y, z, pOrigin); + Vec3_Add(&t->Min, &v, &Blocks.RenderMinBB[t->block]); + Vec3_Add(&t->Max, &v, &Blocks.RenderMaxBB[t->block]); + + dxMin = Math_AbsF(origin->x - t->Min.x); dxMax = Math_AbsF(origin->x - t->Max.x); + dyMin = Math_AbsF(origin->y - t->Min.y); dyMax = Math_AbsF(origin->y - t->Max.y); + dzMin = Math_AbsF(origin->z - t->Min.z); dzMax = Math_AbsF(origin->z - t->Max.z); + dx = min(dxMin, dxMax); dy = min(dyMin, dyMax); dz = min(dzMin, dzMax); + if (dx * dx + dy * dy + dz * dz > reachSq) return false; + + if (intersect(t)) return true; + RayTracer_Step(t); + } + + Logger_Abort("Something went wrong, did over 25,000 iterations in Picking_RayTrace()"); + return false; +} + +static cc_bool ClipBlock(struct RayTracer* t) { + Vec3 scaledDir; + float lenSq, reach; + float t0, t1; + + if (!Game_CanPick(t->block)) return false; + /* This cell falls on the path of the ray. Now perform an additional AABB test, + since some blocks do not occupy a whole cell. */ + if (!Intersection_RayIntersectsBox(t->origin, t->invDir, t->Min, t->Max, &t0, &t1)) return false; + + Vec3_Mul1(&scaledDir, &t->dir, t0); /* scaledDir = dir * t0 */ + Vec3_Add(&t->intersect, &t->origin, &scaledDir); /* intersect = origin + scaledDir */ + + /* Only pick the block if the block is precisely within reach distance. */ + lenSq = Vec3_LengthSquared(&scaledDir); + reach = Entities.CurPlayer->ReachDistance; + + if (lenSq <= reach * reach) { + SetAsValid(t); + } else { + RayTracer_SetInvalid(t); + } + return true; +} + +static const Vec3 picking_adjust = { 0.1f, 0.1f, 0.1f }; +static cc_bool ClipCamera(struct RayTracer* t) { + Vec3 intersect; + float t0, t1; + + if (Blocks.Draw[t->block] == DRAW_GAS || Blocks.Collide[t->block] != COLLIDE_SOLID) return false; + if (!Intersection_RayIntersectsBox(t->origin, t->invDir, t->Min, t->Max, &t0, &t1)) return false; + + /* Need to collide with slightly outside block, to avoid camera clipping issues */ + Vec3_Sub(&t->Min, &t->Min, &picking_adjust); + Vec3_Add(&t->Max, &t->Max, &picking_adjust); + Intersection_RayIntersectsBox(t->origin, t->invDir, t->Min, t->Max, &t0, &t1); + + Vec3_Mul1(&intersect, &t->dir, t0); /* intersect = dir * t0 */ + Vec3_Add(&t->intersect, &t->origin, &intersect); /* intersect = origin + dir * t0 */ + SetAsValid(t); + return true; +} + +void Picking_CalcPickedBlock(const Vec3* origin, const Vec3* dir, float reach, struct RayTracer* t) { + if (!RayTrace(t, origin, dir, reach, ClipBlock)) { + RayTracer_SetInvalid(t); + } +} + +void Picking_ClipCameraPos(const Vec3* origin, const Vec3* dir, float reach, struct RayTracer* t) { + cc_bool noClip = (!Camera.Clipping || Entities.CurPlayer->Hacks.Noclip) + && Entities.CurPlayer->Hacks.CanNoclip; + if (noClip || !World.Loaded || !RayTrace(t, origin, dir, reach, ClipCamera)) { + RayTracer_SetInvalid(t); + Vec3_Mul1(&t->intersect, dir, reach); /* intersect = dir * reach */ + Vec3_Add(&t->intersect, origin, &t->intersect); /* intersect = origin + dir * reach */ + } +} diff --git a/src/Picking.h b/src/Picking.h new file mode 100644 index 0000000..aab774c --- /dev/null +++ b/src/Picking.h @@ -0,0 +1,45 @@ +#ifndef CC_PICKING_H +#define CC_PICKING_H +#include "Vectors.h" +/* +Provides ray tracer functionality for calculating picking/selecting intersection + e.g. calculating block selected in the world by the user, clipping the camera +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ + +/* Implements a voxel ray tracer +http://www.xnawiki.com/index.php/Voxel_traversal +https://web.archive.org/web/20120113051728/http://www.xnawiki.com/index.php?title=Voxel_traversal + +Implementation based on: "A Fast Voxel Traversal Algorithm for Ray Tracing" +John Amanatides, Andrew Woo +http://www.cse.yorku.ca/~amana/research/grid.pdf +http://www.devmaster.net/articles/raytracing_series/A%20faster%20voxel%20traversal%20algorithm%20for%20ray%20tracing.pdf +*/ +struct RayTracer { + IVec3 pos; /* Coordinates of block within world */ + Vec3 origin, dir, invDir; + Vec3 Min, Max; /* Min/max coords of block's bounding box. */ + BlockID block; + IVec3 step; + Vec3 tMax, tDelta; + /* Result only data */ + Vec3 intersect; /* Coords at which the ray exactly intersected this block. */ + IVec3 translatedPos; /* Coords of the neighbouring block that is closest to the player */ + cc_bool valid; /* Whether the ray tracer actually intersected with a block */ + Face closest; /* Face of the intersected block that is closet to the player */ +}; + +/* Marks the given ray tracer as having no result. */ +void RayTracer_SetInvalid(struct RayTracer* t); +/* Initialises the given ray tracer with the given origin and direction. */ +void RayTracer_Init(struct RayTracer* t, const Vec3* origin, const Vec3* dir); +/* Moves to next grid cell position on the ray. */ +void RayTracer_Step(struct RayTracer* t); + +/* Determines the picked block based on the given origin and direction vector. + Marks pickedPos as invalid if a block could not be found due to going outside map boundaries + or not being able to find a suitable candiate within the given reach distance.*/ +void Picking_CalcPickedBlock(const Vec3* origin, const Vec3* dir, float reach, struct RayTracer* t); +void Picking_ClipCameraPos(const Vec3* origin, const Vec3* dir, float reach, struct RayTracer* t); +#endif diff --git a/src/Platform.h b/src/Platform.h new file mode 100644 index 0000000..00c9185 --- /dev/null +++ b/src/Platform.h @@ -0,0 +1,340 @@ +#ifndef CC_PLATFORM_H +#define CC_PLATFORM_H +#include "Core.h" +/* +Abstracts platform specific memory management, I/O, etc +Copyright 2014-2023 ClassiCube | Licensed under BSD-3 +*/ +struct DateTime; + +enum Socket_PollMode { SOCKET_POLL_READ, SOCKET_POLL_WRITE }; +#if defined CC_BUILD_WIN || defined CC_BUILD_XBOX +typedef cc_uintptr cc_socket; +typedef void* cc_file; +#define _NL "\r\n" +#define NATIVE_STR_LEN 300 +#else +typedef int cc_socket; +typedef int cc_file; +#define _NL "\n" +#define NATIVE_STR_LEN 600 +#endif +#define UPDATE_FILE "ClassiCube.update" + +/* Origin points for when seeking in a file. */ +/* NOTE: These have same values as SEEK_SET/SEEK_CUR/SEEK_END, do not change them */ +enum File_SeekFrom { FILE_SEEKFROM_BEGIN, FILE_SEEKFROM_CURRENT, FILE_SEEKFROM_END }; +/* Number of seconds since 01/01/0001 to start of unix time. */ +#define UNIX_EPOCH_SECONDS 62135596800ULL + +extern const cc_result ReturnCode_FileShareViolation; +extern const cc_result ReturnCode_FileNotFound; +extern const cc_result ReturnCode_SocketInProgess; +extern const cc_result ReturnCode_SocketWouldBlock; +extern const cc_result ReturnCode_DirectoryExists; + +/* Whether the launcher and game must both be run in the same process */ +/* (e.g. can't start a separate process on Mobile or Consoles) */ +extern cc_bool Platform_SingleProcess; +/* Suffix added to app name sent to the server */ +extern const char* Platform_AppNameSuffix; +/* Whether the filesystem is readonly (i.e. cannot make chat logs, cache, etc) */ +extern cc_bool Platform_ReadonlyFilesystem; + +#ifdef CC_BUILD_WIN +typedef struct cc_winstring_ { + cc_unichar uni[NATIVE_STR_LEN]; /* String represented using UTF16 format */ + char ansi[NATIVE_STR_LEN]; /* String lossily represented using ANSI format */ +} cc_winstring; +/* Encodes a string in UTF16 and ASCII format, also null terminating the string. */ +void Platform_EncodeString(cc_winstring* dst, const cc_string* src); + +cc_bool Platform_DescribeErrorExt(cc_result res, cc_string* dst, void* lib); +#endif + +/* Initialises the platform specific state. */ +void Platform_Init(void); +/* Frees the platform specific state. */ +void Platform_Free(void); +/* Sets the appropriate default current/working directory. */ +cc_result Platform_SetDefaultCurrentDirectory(int argc, char **argv); +/* Gets the command line arguments passed to the program. */ +int Platform_GetCommandLineArgs(int argc, STRING_REF char** argv, cc_string* args); + +/* Encrypts data in a platform-specific manner. (may not be supported) */ +cc_result Platform_Encrypt(const void* data, int len, cc_string* dst); +/* Decrypts data in a platform-specific manner. (may not be supported) */ +cc_result Platform_Decrypt(const void* data, int len, cc_string* dst); +/* Outputs more detailed information about errors with operating system functions. */ +/* NOTE: This is for general functions like file I/O. If a more specific +describe exists (e.g. Http_DescribeError), that should be preferred. */ +cc_bool Platform_DescribeError(cc_result res, cc_string* dst); + +/* Starts the game with the given arguments. */ +CC_API cc_result Process_StartGame2(const cc_string* args, int numArgs); +/* Terminates the process with the given return code. */ +CC_API void Process_Exit(cc_result code); +/* Starts the platform-specific program to open the given url or filename. */ +/* For example, provide a http:// url to open a website in the user's web browser. */ +CC_API cc_result Process_StartOpen(const cc_string* args); +/* Whether opening URLs is supported by the platform */ +extern cc_bool Process_OpenSupported; + +struct UpdaterBuild { + const char* name; + const char* path; +}; +extern const struct UpdaterInfo { + const char* info; + /* Number of compiled builds available for this platform */ + int numBuilds; + /* Metadata for the compiled builds available for this platform */ + const struct UpdaterBuild builds[2]; // TODO name and path +} Updater_Info; +/* Whether updating is supported by the platform */ +extern cc_bool Updater_Supported; + +/* Attempts to clean up any leftover files from an update */ +cc_bool Updater_Clean(void); +/* Starts the platform-specific method to update then start the game using the UPDATE_FILE file. */ +/* If an error occurs, action indicates which part of the updating process failed. */ +cc_result Updater_Start(const char** action); +/* Returns the last time the application was modified, as a unix timestamp. */ +cc_result Updater_GetBuildTime(cc_uint64* timestamp); +/* Marks the UPDATE_FILE file as being executable. (Needed for some platforms) */ +cc_result Updater_MarkExecutable(void); +/* Sets the last time UPDATE_FILE file was modified, as a unix timestamp. */ +cc_result Updater_SetNewBuildTime(cc_uint64 timestamp); + +/* TODO: Rename _Load2 to _Load on next plugin API version */ +/* Attempts to load a native dynamic library from the given path. */ +CC_API void* DynamicLib_Load2(const cc_string* path); +/* Attempts to get the address of the symbol in the given dynamic library. */ +/* NOTE: Do NOT use this to load OpenGL functions, use GLContext_GetAddress. */ +CC_API void* DynamicLib_Get2(void* lib, const char* name); +/* Outputs more detailed information about errors with the DynamicLib functions. */ +/* NOTE: You MUST call this immediately after DynamicLib_Load2/DynamicLib_Get2 returns NULL. */ +CC_API cc_bool DynamicLib_DescribeError(cc_string* dst); + +/* The default file extension used for dynamic libraries on this platform. */ +extern const cc_string DynamicLib_Ext; +#define DYNAMICLIB_QUOTE(x) #x +#define DynamicLib_Sym(sym) { DYNAMICLIB_QUOTE(sym), (void**)&_ ## sym } +#define DynamicLib_Sym2(name, sym) { name, (void**)&_ ## sym } +#if defined CC_BUILD_OS2 +#define DynamicLib_SymC(sym) { DYNAMICLIB_QUOTE(_ ## sym), (void**)&_ ## sym } +#endif + +CC_API cc_result DynamicLib_Load(const cc_string* path, void** lib); /* OBSOLETE */ +CC_API cc_result DynamicLib_Get(void* lib, const char* name, void** symbol); /* OBSOLETE */ +/* Contains a name and a pointer to variable that will hold the loaded symbol */ +/* static int (APIENTRY *_myGetError)(void); --- (for example) */ +/* static struct DynamicLibSym sym = { "myGetError", (void**)&_myGetError }; */ +struct DynamicLibSym { const char* name; void** symAddr; }; +/* Loads all symbols using DynamicLib_Get2 in the given list */ +/* Returns true if all symbols were successfully retrieved */ +cc_bool DynamicLib_LoadAll(const cc_string* path, const struct DynamicLibSym* syms, int count, void** lib); + +/* Allocates a block of memory, with undetermined contents. Returns NULL on allocation failure. */ +CC_API void* Mem_TryAlloc(cc_uint32 numElems, cc_uint32 elemsSize); +/* Allocates a block of memory, with contents of all 0. Returns NULL on allocation failure. */ +CC_API void* Mem_TryAllocCleared(cc_uint32 numElems, cc_uint32 elemsSize); +/* Reallocates a block of memory, with undetermined contents. Returns NULL on reallocation failure. */ +CC_API void* Mem_TryRealloc(void* mem, cc_uint32 numElems, cc_uint32 elemsSize); + +/* Allocates a block of memory, with undetermined contents. Exits process on allocation failure. */ +CC_API void* Mem_Alloc(cc_uint32 numElems, cc_uint32 elemsSize, const char* place); +/* Allocates a block of memory, with contents of all 0. Exits process on allocation failure. */ +CC_API void* Mem_AllocCleared(cc_uint32 numElems, cc_uint32 elemsSize, const char* place); +/* Reallocates a block of memory, with undetermined contents. Exits process on reallocation failure. */ +CC_API void* Mem_Realloc(void* mem, cc_uint32 numElems, cc_uint32 elemsSize, const char* place); +/* Frees an allocated a block of memory. Does nothing when passed NULL. */ +CC_API void Mem_Free(void* mem); + +/* Sets the contents of a block of memory to the given value. */ +void* Mem_Set(void* dst, cc_uint8 value, unsigned numBytes); +/* Copies a block of memory to another block of memory. */ +/* NOTE: These blocks MUST NOT overlap. */ +void* Mem_Copy(void* dst, const void* src, unsigned numBytes); +/* Moves a block of memory to another block of memory. */ +/* NOTE: These blocks can overlap. */ +void* Mem_Move(void* dst, const void* src, unsigned numBytes); +/* Returns non-zero if the two given blocks of memory have equal contents. */ +int Mem_Equal(const void* a, const void* b, cc_uint32 numBytes); + +/* Logs a debug message to console. */ +void Platform_Log(const char* msg, int len); +void Platform_LogConst(const char* message); +void Platform_Log1(const char* format, const void* a1); +void Platform_Log2(const char* format, const void* a1, const void* a2); +void Platform_Log3(const char* format, const void* a1, const void* a2, const void* a3); +void Platform_Log4(const char* format, const void* a1, const void* a2, const void* a3, const void* a4); + +/* Returns the current UTC time, as number of seconds since 1/1/0001 */ +CC_API TimeMS DateTime_CurrentUTC(void); +/* Returns the current local Time. */ +CC_API void DateTime_CurrentLocal(struct DateTime* t); +/* Takes a platform-specific stopwatch measurement. */ +/* NOTE: The value returned is platform-specific - do NOT try to interpret the value. */ +CC_API cc_uint64 Stopwatch_Measure(void); +/* Returns total elapsed microseconds between two stopwatch measurements. */ +CC_API cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end); +/* Returns total elapsed milliseconds between two stopwatch measurements. */ +int Stopwatch_ElapsedMS(cc_uint64 beg, cc_uint64 end); + +/* Attempts to create a new directory. */ +CC_API cc_result Directory_Create(const cc_string* path); +/* Callback function invoked for each file found. */ +typedef void (*Directory_EnumCallback)(const cc_string* filename, void* obj); +/* Invokes a callback function on all filenames in the given directory (and its sub-directories) */ +CC_API cc_result Directory_Enum(const cc_string* path, void* obj, Directory_EnumCallback callback); +/* Returns non-zero if the given file exists. */ +CC_API int File_Exists(const cc_string* path); +void Directory_GetCachePath(cc_string* path); + +/* Attempts to create a new (or overwrite) file for writing. */ +/* NOTE: If the file already exists, its contents are discarded. */ +cc_result File_Create(cc_file* file, const cc_string* path); +/* Attempts to open an existing file for reading. */ +cc_result File_Open(cc_file* file, const cc_string* path); +/* Attempts to open an existing or create a new file for reading and writing. */ +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path); +/* Attempts to read data from the file. */ +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead); +/* Attempts to write data to the file. */ +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote); +/* Attempts to close the given file. */ +cc_result File_Close(cc_file file); +/* Attempts to seek to a position in the given file. */ +cc_result File_Seek(cc_file file, int offset, int seekType); +/* Attempts to get the current position in the given file. */ +cc_result File_Position(cc_file file, cc_uint32* pos); +/* Attempts to retrieve the length of the given file. */ +cc_result File_Length(cc_file file, cc_uint32* len); + +typedef void (*Thread_StartFunc)(void); +/* Blocks the current thread for the given number of milliseconds. */ +CC_API void Thread_Sleep(cc_uint32 milliseconds); +/* Initialises and starts a new thread that runs the given function. */ +/* NOTE: Threads must either be detached or joined, otherwise data leaks. */ +CC_API void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name); +/* Frees the platform specific persistent data associated with the thread. */ +/* NOTE: Once a thread has been detached, Thread_Join can no longer be used. */ +CC_API void Thread_Detach(void* handle); +/* Blocks the current thread, until the given thread has finished. */ +/* NOTE: This cannot be used on a thread that has been detached. */ +CC_API void Thread_Join(void* handle); + +/* Allocates a new mutex. (used to synchronise access to a shared resource) */ +CC_API void* Mutex_Create(const char* name); +/* Frees an allocated mutex. */ +CC_API void Mutex_Free(void* handle); +/* Locks the given mutex, blocking other threads from entering. */ +CC_API void Mutex_Lock(void* handle); +/* Unlocks the given mutex, allowing other threads to enter. */ +CC_API void Mutex_Unlock(void* handle); + +/* Allocates a new waitable. (used to conditionally wake-up a blocked thread) */ +CC_API void* Waitable_Create(const char* name); +/* Frees an allocated waitable. */ +CC_API void Waitable_Free(void* handle); +/* Signals a waitable, waking up blocked threads. */ +CC_API void Waitable_Signal(void* handle); +/* Blocks the calling thread until the waitable gets signalled. */ +CC_API void Waitable_Wait(void* handle); +/* Blocks the calling thread until the waitable gets signalled, or milliseconds delay passes. */ +CC_API void Waitable_WaitFor(void* handle, cc_uint32 milliseconds); + +/* Calls SysFonts_Register on each font that is available on this platform. */ +void Platform_LoadSysFonts(void); + +#define CC_SOCKETADDR_MAXSIZE 512 +#define SOCKET_MAX_ADDRS 5 + +typedef struct cc_sockaddr_ { + int size; /* Actual size of the raw socket address */ + cc_uint8 data[CC_SOCKETADDR_MAXSIZE]; /* Raw socket address (e.g. sockaddr_in) */ +} cc_sockaddr; + +/* Checks if the given socket is currently readable (i.e. has data available to read) */ +/* NOTE: A closed socket is also considered readable */ +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable); +/* Checks if the given socket is currently writable (i.e. has finished connecting) */ +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable); +/* If the input represents an IP address, then parses the input into a single IP address */ +/* Otherwise, attempts to resolve the input via DNS into one or more IP addresses */ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs); + +/* Allocates a new socket and then begins connecting to the given address */ +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking); +/* Attempts to read data from the given socket */ +/* NOTE: A closed socket may set modified to 0, but still return 'success' (i.e. 0) */ +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified); +/* Attempts to write data to the given socket */ +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified); +/* Attempts to close the given socket */ +void Socket_Close(cc_socket s); +/* Attempts to write all data to the given socket, returning ERR_END_OF_STREAM if it could not */ +cc_result Socket_WriteAll(cc_socket socket, const cc_uint8* data, cc_uint32 count); + +#ifdef CC_BUILD_MOBILE +void Platform_ShareScreenshot(const cc_string* filename); +#endif + +#ifdef CC_BUILD_ANDROID +#include +extern jclass App_Class; +extern jobject App_Instance; +extern JavaVM* VM_Ptr; +void Platform_TryLogJavaError(void); + +#define JavaGetCurrentEnv(env) (*VM_Ptr)->AttachCurrentThread(VM_Ptr, &env, NULL) +#define JavaMakeConst(env, str) (*env)->NewStringUTF(env, str) + +#define JavaRegisterNatives(env, methods) (*env)->RegisterNatives(env, App_Class, methods, Array_Elems(methods)); +#define JavaGetIMethod(env, name, sig) (*env)->GetMethodID(env, App_Class, name, sig) +#define JavaGetSMethod(env, name, sig) (*env)->GetStaticMethodID(env, App_Class, name, sig) + +/* Creates a string from the given java string. buffer must be at least NATIVE_STR_LEN long. */ +/* NOTE: Don't forget to call env->ReleaseStringUTFChars. Only works with ASCII strings. */ +cc_string JavaGetString(JNIEnv* env, jstring str, char* buffer); +/* Allocates a java string from the given string. */ +jobject JavaMakeString(JNIEnv* env, const cc_string* str); +/* Allocates a java byte array from the given block of memory. */ +jbyteArray JavaMakeBytes(JNIEnv* env, const void* src, cc_uint32 len); +/* Calls a method in the activity class that returns nothing. */ +void JavaCallVoid(JNIEnv* env, const char* name, const char* sig, jvalue* args); +/* Calls a method in the activity class that returns a jint. */ +jlong JavaCallLong(JNIEnv* env, const char* name, const char* sig, jvalue* args); +/* Calls a method in the activity class that returns a jobject. */ +jobject JavaCallObject(JNIEnv* env, const char* name, const char* sig, jvalue* args); +/* Calls a method in the activity class that takes a string and returns nothing. */ +void JavaCall_String_Void(const char* name, const cc_string* value); +/* Calls a method in the activity class that takes no arguments and returns a string. */ +void JavaCall_Void_String(const char* name, cc_string* dst); +/* Calls a method in the activity class that takes a string and returns a string. */ +void JavaCall_String_String(const char* name, const cc_string* arg, cc_string* dst); + +/* Calls an instance method in the activity class that returns nothing */ +#define JavaICall_Void(env, method, args) (*env)->CallVoidMethodA(env, App_Instance, method, args) +/* Calls an instance method in the activity class that returns a jint */ +#define JavaICall_Int(env, method, args) (*env)->CallIntMethodA(env, App_Instance, method, args) +/* Calls an instance method in the activity class that returns a jlong */ +#define JavaICall_Long(env, method, args) (*env)->CallLongMethodA(env, App_Instance, method, args) +/* Calls an instance method in the activity class that returns a jfloat */ +#define JavaICall_Float(env,method, args) (*env)->CallFloatMethodA(env, App_Instance, method, args) +/* Calls an instance method in the activity class that returns a jobject */ +#define JavaICall_Obj(env, method, args) (*env)->CallObjectMethodA(env,App_Instance, method, args) + +/* Calls a static method in the activity class that returns nothing */ +#define JavaSCall_Void(env, method, args) (*env)->CallStaticVoidMethodA(env, App_Class, method, args) +/* Calls a static method in the activity class that returns a jint */ +#define JavaSCall_Int(env, method, args) (*env)->CallStaticIntMethodA(env, App_Class, method, args) +/* Calls a static method in the activity class that returns a jlong */ +#define JavaSCall_Long(env, method, args) (*env)->CallStaticLongMethodA(env, App_Class, method, args) +/* Calls a static method in the activity class that returns a jfloat */ +#define JavaSCall_Float(env,method, args) (*env)->CallStaticFloatMethodA(env, App_Class, method, args) +/* Calls a static method in the activity class that returns a jobject */ +#define JavaSCall_Obj(env, method, args) (*env)->CallStaticObjectMethodA(env,App_Class, method, args) +#endif +#endif diff --git a/src/Platform_3DS.c b/src/Platform_3DS.c new file mode 100644 index 0000000..9ee4703 --- /dev/null +++ b/src/Platform_3DS.c @@ -0,0 +1,457 @@ +#include "Core.h" +#if defined CC_BUILD_3DS +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include <3ds.h> +#include +#include "_PlatformConsole.h" + +#define US_PER_SEC 1000000LL +#define NS_PER_MS 1000000LL + +const cc_result ReturnCode_FileShareViolation = 1000000000; /* TODO: not used apparently */ +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " 3DS"; + +// https://gbatemp.net/threads/homebrew-development.360646/page-245 +// 3DS defaults to stack size of *32 KB*.. way too small +unsigned int __stacksize__ = 256 * 1024; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Log(const char* msg, int len) { + // output to debug service (visible in Citra with log level set to "Debug.Emulated:Debug", or on console via remote gdb) + svcOutputDebugString(msg, len); +} + +TimeMS DateTime_CurrentUTC(void) { + struct timeval cur; + gettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct timeval cur; + struct tm loc_time; + gettimeofday(&cur, NULL); + localtime_r(&cur.tv_sec, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + +cc_uint64 Stopwatch_Measure(void) { + return svcGetSystemTick(); +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + // TODO: This doesn't seem to accurately measure time in Citra. + // hopefully it works better on a real 3DS? + + // See CPU_TICKS_PER_USEC in libctru/include/3ds/os.h + return (end - beg) * US_PER_SEC / SYSCLOCK_ARM11; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("sdmc:/3ds/ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + return mkdir(str, 0666) == -1 ? errno : 0; // FS has no permissions anyways +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + struct stat sb; + GetNativePath(str, path); + + return stat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + struct dirent* entry; + char* src; + int len, res, is_dir; + + GetNativePath(str, dirPath); + DIR* dirPtr = opendir(str); + if (!dirPtr) return errno; + + /* POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." */ + /* errno is sometimes leftover from previous calls, so always reset it before readdir gets called */ + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + src = entry->d_name; + + len = String_Length(src); + String_AppendUtf8(&path, src, len); + is_dir = entry->d_type == DT_DIR; + /* TODO: fallback to stat when this fails */ + + if (is_dir) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; /* return code from readdir */ + closedir(dirPtr); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + *file = open(str, mode, 0666); // FS has no permissions anyways + return *file == -1 ? errno : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + *bytesRead = read(file, data, count); + return *bytesRead == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + *bytesWrote = write(file, data, count); + return *bytesWrote == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + return close(file) == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return lseek(file, offset, modes[seekType]) == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = lseek(file, 0, SEEK_CUR); + return *pos == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + struct stat st; + if (fstat(file, &st) == -1) { *len = -1; return errno; } + *len = st.st_size; return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + svcSleepThread(milliseconds * NS_PER_MS); +} + +static void Exec3DSThread(void* param) { + ((Thread_StartFunc)param)(); +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + //TODO: Not quite correct, but eh + *handle = threadCreate(Exec3DSThread, (void*)func, stackSize, 0x3f, -2, false); +} + +void Thread_Detach(void* handle) { + Thread thread = (Thread)handle; + threadDetach(thread); +} + +void Thread_Join(void* handle) { + Thread thread = (Thread)handle; + threadJoin(thread, U64_MAX); + threadFree(thread); +} + +void* Mutex_Create(const char* name) { + LightLock* lock = (LightLock*)Mem_Alloc(1, sizeof(LightLock), "mutex"); + LightLock_Init(lock); + return lock; +} + +void Mutex_Free(void* handle) { + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + LightLock_Lock((LightLock*)handle); +} + +void Mutex_Unlock(void* handle) { + LightLock_Unlock((LightLock*)handle); +} + +void* Waitable_Create(const char* name) { + LightEvent* event = (LightEvent*)Mem_Alloc(1, sizeof(LightEvent), "waitable"); + LightEvent_Init(event, RESET_ONESHOT); + return event; +} + +void Waitable_Free(void* handle) { + Mem_Free(handle); +} + +void Waitable_Signal(void* handle) { + LightEvent_Signal((LightEvent*)handle); +} + +void Waitable_Wait(void* handle) { + LightEvent_Wait((LightEvent*)handle); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + s64 timeout_ns = milliseconds * (1000 * 1000); // milliseconds to nanoseconds + LightEvent_WaitTimeout((LightEvent*)handle, timeout_ns); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char str[NATIVE_STR_LEN]; + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int i = 0; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + if (inet_aton(str, &addr4->sin_addr) > 0) { + // TODO eliminate this path? + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(*addr4); + *numValidAddrs = 1; + return 0; + } + + hints.ai_family = AF_INET; // TODO: you need this, otherwise resolving dl.dropboxusercontent.com crashes in Citra. probably something to do with IPv6 addresses + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + int res = getaddrinfo(str, portRaw, &hints, &result); + if (res == -NO_DATA) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next, i++) + { + if (!cur->ai_addrlen) break; + // TODO citra returns empty addresses past first one? does that happen on real hardware too? + + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); + } + + freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + + *s = socket(raw->sa_family, SOCK_STREAM, 0); // https://www.3dbrew.org/wiki/SOCU:socket + if (*s == -1) return errno; + + if (nonblocking) { + int flags = fcntl(*s, F_GETFL, 0); + if (flags >= 0) fcntl(*s, F_SETFL, flags | O_NONBLOCK); + } + + res = connect(*s, raw, addr->size); + return res == -1 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = recv(s, data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return errno; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = send(s, data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + + *modified = 0; return errno; +} + +void Socket_Close(cc_socket s) { + shutdown(s, SHUT_RDWR); + close(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + if (poll(&pfd, 1, 0) == -1) { *success = false; return errno; } + + /* to match select, closed socket still counts as readable */ + int flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + // Actual 3DS hardware returns INPROGRESS error code if connect is still in progress + // Which is different from POSIX: + // https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + Platform_Log1("--write poll failed-- = %i", &res); + if (res == -26) res = 0; + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +#define SOC_CTX_ALIGN 0x1000 +#define SOC_CTX_SIZE 0x1000 * 128 + +static void CreateRootDirectory(const char* path) { + // create root directories (no permissions anyways) + int res = mkdir(path, 0666); + if (res >= 0) return; + + int err = errno; + Platform_Log2("mkdir %c FAILED: %i", path, &err); +} + +void Platform_Init(void) { + // Take full advantage of new 3DS if running on it + osSetSpeedupEnable(true); + + // create root directories (no permissions anyways) + CreateRootDirectory("sdmc:/3ds"); + CreateRootDirectory("sdmc:/3ds/ClassiCube"); + + // See https://github.com/devkitPro/libctru/blob/master/libctru/include/3ds/services/soc.h + // * @param context_addr Address of a page-aligned (0x1000) buffer to be used. + // * @param context_size Size of the buffer, a multiple of 0x1000. + // * @note The specified context buffer can no longer be accessed by the process which called this function, since the userland permissions for this block are set to no-access. + void* buffer = memalign(SOC_CTX_ALIGN, SOC_CTX_SIZE); + if (!buffer) return; + socInit(buffer, SOC_CTX_SIZE); +} + +void Platform_Free(void) { + socExit(); + // TODO free soc buffer? probably no point +} + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + // doesn't really matter if called multiple times + psInit(); + return PS_GetDeviceId(key); +} +#endif diff --git a/src/Platform_Android.c b/src/Platform_Android.c new file mode 100644 index 0000000..e3b3a74 --- /dev/null +++ b/src/Platform_Android.c @@ -0,0 +1,253 @@ +#include "Core.h" +#if defined CC_BUILD_ANDROID +#include "Chat.h" +#include "Constants.h" +#include "Errors.h" +#include "Funcs.h" +#include "String.h" +#include "Platform.h" +#include +#include +#include + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Log(const char* msg, int len) { + char tmp[2048 + 1]; + len = min(len, 2048); + + Mem_Copy(tmp, msg, len); tmp[len] = '\0'; + /* log using logchat */ + __android_log_write(ANDROID_LOG_DEBUG, "ClassiCube", tmp); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Process/Module------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Process_StartOpen(const cc_string* args) { + JavaCall_String_Void("startOpen", args); + return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Updater----------------------------------------------------------* +*#########################################################################################################################*/ +const struct UpdaterInfo Updater_Info = { + "&eRedownload and reinstall to update", 0 +}; +cc_bool Updater_Clean(void) { return true; } + +cc_result Updater_GetBuildTime(cc_uint64* t) { + JNIEnv* env; + JavaGetCurrentEnv(env); + *t = JavaCallLong(env, "getApkUpdateTime", "()J", NULL); + return 0; +} + +cc_result Updater_Start(const char** action) { *action = "Updating game"; return ERR_NOT_SUPPORTED; } +cc_result Updater_MarkExecutable(void) { return 0; } +cc_result Updater_SetNewBuildTime(cc_uint64 t) { return ERR_NOT_SUPPORTED; } + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_TryLogJavaError(void) { + JNIEnv* env; + jthrowable err; + JavaGetCurrentEnv(env); + + err = (*env)->ExceptionOccurred(env); + if (!err) return; + + Platform_LogConst("PANIC"); + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + /* TODO actually do something */ +} + +void Platform_ShareScreenshot(const cc_string* filename) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + String_InitArray(path, pathBuffer); + + JavaCall_String_String("shareScreenshot", filename, &path); + if (!path.length) return; + + Chat_AddRaw("&cError sharing screenshot"); + Chat_Add1(" &c%s", &path); +} + +void Directory_GetCachePath(cc_string* path) { + // TODO cache method ID + JavaCall_Void_String("getGameCacheDirectory", path); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Configuration-------------------------------------------------------* +*#########################################################################################################################*/ +#include "Window.h" +cc_result Platform_SetDefaultCurrentDirectory(int argc, char **argv) { + cc_string dir; char dirBuffer[FILENAME_SIZE + 1]; + String_InitArray_NT(dir, dirBuffer); + + JavaCall_Void_String("getGameDataDirectory", &dir); + dir.buffer[dir.length] = '\0'; + Platform_Log1("DATA DIR: %s|", &dir); + + int res = chdir(dir.buffer) == -1 ? errno : 0; + if (!res) return 0; + + // show the path to the user + // TODO there must be a better way + Window_ShowDialog("Failed to set working directory to", dir.buffer); + return res; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Java Interop--------------------------------------------------------* +*#########################################################################################################################*/ +jclass App_Class; +jobject App_Instance; +JavaVM* VM_Ptr; + +/* JNI helpers */ +cc_string JavaGetString(JNIEnv* env, jstring str, char* buffer) { + const char* src; int len; + cc_string dst; + src = (*env)->GetStringUTFChars(env, str, NULL); + len = (*env)->GetStringUTFLength(env, str); + + dst.buffer = buffer; + dst.length = 0; + dst.capacity = NATIVE_STR_LEN; + String_AppendUtf8(&dst, src, len); + + (*env)->ReleaseStringUTFChars(env, str, src); + return dst; +} + +jobject JavaMakeString(JNIEnv* env, const cc_string* str) { + cc_uint8 tmp[2048 + 4]; + cc_uint8* cur; + int i, len = 0; + + for (i = 0; i < str->length && len < 2048; i++) { + cur = tmp + len; + len += Convert_CP437ToUtf8(str->buffer[i], cur); + } + tmp[len] = '\0'; + return (*env)->NewStringUTF(env, (const char*)tmp); +} + +jbyteArray JavaMakeBytes(JNIEnv* env, const void* src, cc_uint32 len) { + if (!len) return NULL; + jbyteArray arr = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, arr, 0, len, src); + return arr; +} + +void JavaCallVoid(JNIEnv* env, const char* name, const char* sig, jvalue* args) { + jmethodID method = (*env)->GetMethodID(env, App_Class, name, sig); + (*env)->CallVoidMethodA(env, App_Instance, method, args); +} + +jlong JavaCallLong(JNIEnv* env, const char* name, const char* sig, jvalue* args) { + jmethodID method = (*env)->GetMethodID(env, App_Class, name, sig); + return (*env)->CallLongMethodA(env, App_Instance, method, args); +} + +jobject JavaCallObject(JNIEnv* env, const char* name, const char* sig, jvalue* args) { + jmethodID method = (*env)->GetMethodID(env, App_Class, name, sig); + return (*env)->CallObjectMethodA(env, App_Instance, method, args); +} + +void JavaCall_String_Void(const char* name, const cc_string* value) { + JNIEnv* env; + jvalue args[1]; + JavaGetCurrentEnv(env); + + args[0].l = JavaMakeString(env, value); + JavaCallVoid(env, name, "(Ljava/lang/String;)V", args); + (*env)->DeleteLocalRef(env, args[0].l); +} + +static void ReturnString(JNIEnv* env, jobject obj, cc_string* dst) { + const jchar* src; + jsize len; + if (!obj) return; + + src = (*env)->GetStringChars(env, obj, NULL); + len = (*env)->GetStringLength(env, obj); + String_AppendUtf16(dst, src, len * 2); + (*env)->ReleaseStringChars(env, obj, src); + (*env)->DeleteLocalRef(env, obj); +} + +void JavaCall_Void_String(const char* name, cc_string* dst) { + JNIEnv* env; + jobject obj; + JavaGetCurrentEnv(env); + + obj = JavaCallObject(env, name, "()Ljava/lang/String;", NULL); + ReturnString(env, obj, dst); +} + +void JavaCall_String_String(const char* name, const cc_string* arg, cc_string* dst) { + JNIEnv* env; + jobject obj; + jvalue args[1]; + JavaGetCurrentEnv(env); + + args[0].l = JavaMakeString(env, arg); + obj = JavaCallObject(env, name, "(Ljava/lang/String;)Ljava/lang/String;", args); + ReturnString(env, obj, dst); + (*env)->DeleteLocalRef(env, args[0].l); +} + + +/*########################################################################################################################* +*----------------------------------------------------Initialisation-------------------------------------------------------* +*#########################################################################################################################*/ +extern void android_main(void); +static void JNICALL java_updateInstance(JNIEnv* env, jobject instance) { + Platform_LogConst("App instance updated!"); + App_Instance = (*env)->NewGlobalRef(env, instance); + /* TODO: Do we actually need to remove that global ref later? */ +} + +/* Called eventually by the activity java class to actually start the game */ +static void JNICALL java_runGameAsync(JNIEnv* env, jobject instance) { + void* thread; + java_updateInstance(env, instance); + + Platform_LogConst("Running game async!"); + /* The game must be run on a separate thread, as blocking the */ + /* main UI thread will cause a 'App not responding..' messagebox */ + Thread_Run(&thread, android_main, 1024 * 1024, "Game"); // TODO check stack size needed + Thread_Detach(thread); +} +static const JNINativeMethod methods[] = { + { "updateInstance", "()V", java_updateInstance }, + { "runGameAsync", "()V", java_runGameAsync } +}; + +/* This method is automatically called by the Java VM when the */ +/* activity java class calls 'System.loadLibrary("classicube");' */ +CC_API jint JNI_OnLoad(JavaVM* vm, void* reserved) { + jclass klass; + JNIEnv* env; + VM_Ptr = vm; + JavaGetCurrentEnv(env); + + klass = (*env)->FindClass(env, "com/classicube/MainActivity"); + App_Class = (*env)->NewGlobalRef(env, klass); + JavaRegisterNatives(env, methods); + return JNI_VERSION_1_4; +} +#endif diff --git a/src/Platform_BeOS.cpp b/src/Platform_BeOS.cpp new file mode 100644 index 0000000..77a3bca --- /dev/null +++ b/src/Platform_BeOS.cpp @@ -0,0 +1,110 @@ +#include "Core.h" +#if defined CC_BUILD_BEOS || defined CC_BUILD_HAIKU +extern "C" { +#include "Platform.h" +#include "String.h" +#include "Funcs.h" +#include "Utils.h" +} +#include +#include +#include + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_Measure(void) { + return system_time(); +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return end - beg; +} + +cc_result Process_StartOpen(const cc_string* args) { + static const cc_string https_protocol = String_FromConst("https://"); + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, args); + + cc_bool https = String_CaselessStarts(args, &https_protocol); + const char* mime = https ? "application/x-vnd.Be.URL.https" : "application/x-vnd.Be.URL.http"; + + char* argv[] = { str, NULL }; + return be_roster->Launch(mime, 1, argv); +} + + +/*########################################################################################################################* +*-----------------------------------------------------BeOS threading------------------------------------------------------* +*#########################################################################################################################*/ +// NOTE: BeOS only, as haiku uses the more efficient pthreads implementation in Platform_Posix.c +#if defined CC_BUILD_BEOS +void Thread_Sleep(cc_uint32 milliseconds) { snooze(milliseconds * 1000); } + +static int32 ExecThread(void* param) { + ((Thread_StartFunc)param)(); + return 0; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + thread_id thread = spawn_thread(ExecThread, name, B_NORMAL_PRIORITY, func); + *handle = (void*)thread; + + resume_thread(thread); +} + +void Thread_Detach(void* handle) { } + +void Thread_Join(void* handle) { + thread_id thread = (thread_id)handle; + wait_for_thread(thread, NULL); +} + +void* Mutex_Create(const char* name) { + sem_id id = create_sem(1, name); + return (void*)id; +} + +void Mutex_Free(void* handle) { + sem_id id = (sem_id)handle; + delete_sem(id); +} + +void Mutex_Lock(void* handle) { + sem_id id = (sem_id)handle; + acquire_sem(id); +} + +void Mutex_Unlock(void* handle) { + sem_id id = (sem_id)handle; + release_sem(id); +} + +void* Waitable_Create(const char* name) { + sem_id id = create_sem(0, name); + return (void*)id; +} + +void Waitable_Free(void* handle) { + sem_id id = (sem_id)handle; + delete_sem(id); +} + +void Waitable_Signal(void* handle) { + sem_id id = (sem_id)handle; + release_sem(id); +} + +void Waitable_Wait(void* handle) { + sem_id id = (sem_id)handle; + acquire_sem(id); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + int microseconds = milliseconds * 1000; + sem_id id = (sem_id)handle; + acquire_sem_etc(id, 1, B_RELATIVE_TIMEOUT, microseconds); +} +#endif +#endif diff --git a/src/Platform_Dreamcast.c b/src/Platform_Dreamcast.c new file mode 100644 index 0000000..bd064a0 --- /dev/null +++ b/src/Platform_Dreamcast.c @@ -0,0 +1,537 @@ +#include "Core.h" +#if defined CC_BUILD_DREAMCAST +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "_PlatformConsole.h" +KOS_INIT_FLAGS(INIT_DEFAULT | INIT_NET); + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " Dreamcast"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return end - beg; +} + +cc_uint64 Stopwatch_Measure(void) { + return timer_us_gettime64(); +} + +static uint32 str_offset; +extern cc_bool window_inited; +#define MAX_ONSCREEN_LINES 20 + +static void LogOnscreen(const char* msg, int len) { + char buffer[50]; + cc_string str; + uint32 secs, ms; + timer_ms_gettime(&secs, &ms); + + String_InitArray_NT(str, buffer); + String_Format2(&str, "[%p2.%p3] ", &secs, &ms); + String_AppendAll(&str, msg, len); + buffer[str.length] = '\0'; + + uint32 line_offset = (10 + (str_offset * BFONT_HEIGHT)) * vid_mode->width; + bfont_draw_str(vram_s + line_offset, vid_mode->width, 1, buffer); + str_offset = (str_offset + 1) % MAX_ONSCREEN_LINES; +} + +void Platform_Log(const char* msg, int len) { + dbgio_write_buffer_xlat(msg, len); + dbgio_write_buffer_xlat("\n", 1); + + if (window_inited) return; + // Log details on-screen for initial model initing etc + // (this can take around 40 seconds on average) + LogOnscreen(msg, len); +} + +TimeMS DateTime_CurrentUTC(void) { + uint32 secs, ms; + timer_ms_gettime(&secs, &ms); + + time_t boot_time = rtc_boot_time(); + // workaround when RTC clock hasn't been setup + int boot_time_2000 = 946684800; + int boot_time_2024 = 1704067200; + if (boot_time < boot_time_2000) boot_time = boot_time_2024; + + cc_uint64 curSecs = boot_time + secs; + return curSecs + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + uint32 secs, ms; + time_t total_secs; + struct tm loc_time; + + timer_ms_gettime(&secs, &ms); + total_secs = rtc_boot_time() + secs; + localtime_r(&total_secs, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static cc_string root_path = String_FromConst("/cd/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + int res = fs_mkdir(str); + int err = res == -1 ? errno : 0; + + // Filesystem returns EINVAL when operation unsupported (e.g. CD system) + // so rather than logging an error, just pretend it already exists + if (err == EINVAL) err = EEXIST; + return err; +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + struct stat sb; + GetNativePath(str, path); + return fs_stat(str, &sb, 0) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + int res; + // CD filesystem loader doesn't usually set errno + // when it can't find the requested file + errno = 0; + + GetNativePath(str, dirPath); + int fd = fs_open(str, O_DIR | O_RDONLY); + if (fd < 0) return errno; + + String_InitArray(path, pathBuffer); + dirent_t* entry; + errno = 0; + + while ((entry = fs_readdir(fd))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + // ignore . and .. entry (PSP does return them) + // TODO: Does Dreamcast? + char* src = entry->name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + + // negative size indicates a directory entry + if (entry->size < 0) { + res = Directory_Enum(&path, obj, callback); + if (res) break; + } else { + callback(&path, obj); + } + } + + int err = errno; // save error from fs_readdir + fs_close(fd); + return err; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + // CD filesystem loader doesn't usually set errno + // when it can't find the requested file + errno = 0; + + int res = fs_open(str, mode); + *file = res; + + int err = res == -1 ? errno : 0; + if (res == -1 && err == 0) err = ENOENT; + return err; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + int res = fs_read(file, data, count); + *bytesRead = res; + return res == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + int res = fs_write(file, data, count); + *bytesWrote = res; + return res == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + int res = fs_close(file); + return res == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + + int res = fs_seek(file, offset, modes[seekType]); + return res == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + int res = fs_seek(file, 0, SEEK_CUR); + *pos = res; + return res == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + int res = fs_total(file); + *len = res; + return res == -1 ? errno : 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +// !!! NOTE: Dreamcast is configured to use preemptive multithreading !!! +void Thread_Sleep(cc_uint32 milliseconds) { + thd_sleep(milliseconds); +} + +static void* ExecThread(void* param) { + Thread_StartFunc func = (Thread_StartFunc)param; + (func)(); + return NULL; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + kthread_attr_t attrs = { 0 }; + attrs.stack_size = stackSize; + attrs.label = name; + *handle = thd_create_ex(&attrs, ExecThread, func); +} + +void Thread_Detach(void* handle) { + thd_detach((kthread_t*)handle); +} + +void Thread_Join(void* handle) { + thd_join((kthread_t*)handle, NULL); +} + +void* Mutex_Create(const char* name) { + mutex_t* ptr = (mutex_t*)Mem_Alloc(1, sizeof(mutex_t), "mutex"); + int res = mutex_init(ptr, MUTEX_TYPE_NORMAL); + if (res) Logger_Abort2(errno, "Creating mutex"); + return ptr; +} + +void Mutex_Free(void* handle) { + int res = mutex_destroy((mutex_t*)handle); + if (res) Logger_Abort2(errno, "Destroying mutex"); + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + int res = mutex_lock((mutex_t*)handle); + if (res) Logger_Abort2(errno, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + int res = mutex_unlock((mutex_t*)handle); + if (res) Logger_Abort2(errno, "Unlocking mutex"); +} + +void* Waitable_Create(const char* name) { + semaphore_t* ptr = (semaphore_t*)Mem_Alloc(1, sizeof(semaphore_t), "waitable"); + int res = sem_init(ptr, 0); + if (res) Logger_Abort2(errno, "Creating waitable"); + return ptr; +} + +void Waitable_Free(void* handle) { + int res = sem_destroy((semaphore_t*)handle); + if (res) Logger_Abort2(errno, "Destroying waitable"); + Mem_Free(handle); +} + +void Waitable_Signal(void* handle) { + int res = sem_signal((semaphore_t*)handle); + if (res < 0) Logger_Abort2(errno, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + int res = sem_wait((semaphore_t*)handle); + if (res < 0) Logger_Abort2(errno, "Event wait"); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + int res = sem_wait_timed((semaphore_t*)handle, milliseconds); + if (res >= 0) return; + + int err = errno; + if (err != ETIMEDOUT) Logger_Abort2(err, "Event timed wait"); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char str[NATIVE_STR_LEN]; + + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int res, i = 0; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + // getaddrinfo IP address resolution was only added in Nov 2023 + // https://github.com/KallistiOS/KallistiOS/pull/358 + // So include this special case for backwards compatibility + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + if (inet_pton(AF_INET, str, &addr4->sin_addr) > 0) { + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(struct sockaddr_in); + *numValidAddrs = 1; + return 0; + } + + res = getaddrinfo(str, portRaw, &hints, &result); + if (res == EAI_NONAME) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next, i++) + { + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); + } + + freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + cc_result res; + + *s = socket(raw->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (*s == -1) return errno; + + if (nonblocking) { + fcntl(*s, F_SETFL, O_NONBLOCK); + } + + res = connect(*s, raw, addr->size); + return res == -1 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = recv(s, data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return errno; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = send(s, data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + *modified = 0; return errno; +} + +void Socket_Close(cc_socket s) { + shutdown(s, SHUT_RDWR); + close(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + int flags; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + if (poll(&pfd, 1, 0) == -1) { *success = false; return errno; } + + /* to match select, closed socket still counts as readable */ + flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static kos_blockdev_t sd_dev; +static uint8 partition_type; + +static void InitSDCard(void) { + if (sd_init()) { + // Both SD card and debug interface use the serial port + // So if initing SD card fails, need to restore serial port state for debug logging + scif_init(); + Platform_LogConst("Failed to init SD card"); return; + } + + if (sd_blockdev_for_partition(0, &sd_dev, &partition_type)) { + Platform_LogConst("Unable to find first partition on SD card"); return; + } + + if (fs_fat_init()) { + Platform_LogConst("Failed to init FAT filesystem"); return; + } + + if (fs_fat_mount("/sd", &sd_dev, FS_FAT_MOUNT_READWRITE)) { + Platform_LogConst("Failed to mount SD card"); return; + } + + root_path = String_FromReadonly("/sd/ClassiCube"); + fs_mkdir("/sd/ClassiCube"); + Platform_ReadonlyFilesystem = false; +} + +static void InitModem(void) { + int err; + Platform_LogConst("Initialising modem.."); + + if (!modem_init()) { + Platform_LogConst("Modem initing failed"); return; + } + ppp_init(); + + Platform_LogConst("Dialling modem.. (can take ~20 seconds)"); + err = ppp_modem_init("111111111111", 0, NULL); + if (err) { + Platform_Log1("Establishing link failed (%i)", &err); return; + } + + ppp_set_login("dream", "dreamcast"); + + Platform_LogConst("Connecting link.. (can take ~20 seconds)"); + err = ppp_connect(); + if (err) { + Platform_Log1("Connecting link failed (%i)", &err); return; + } +} + +void Platform_Init(void) { + Platform_ReadonlyFilesystem = true; + InitSDCard(); + + if (net_default_dev) return; + // in case Broadband Adapter isn't active + InitModem(); + // give some time for messages to stay on-screen + Platform_LogConst("Starting in 5 seconds.."); + Thread_Sleep(5000); +} +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_GCWii.c b/src/Platform_GCWii.c new file mode 100644 index 0000000..c89f342 --- /dev/null +++ b/src/Platform_GCWii.c @@ -0,0 +1,619 @@ +#include "Core.h" +#if defined CC_BUILD_GCWII +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HW_RVL +#include +#endif +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; /* TODO: not used apparently */ +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = -EINPROGRESS; // net_XYZ error results are negative +const cc_result ReturnCode_SocketWouldBlock = -EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +#ifdef HW_RVL +const char* Platform_AppNameSuffix = " Wii"; +#else +const char* Platform_AppNameSuffix = " GameCube"; +#endif + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +// To see these log messages: +// 1) In the UI, make sure 'Show log configuration' checkbox is checked in View menu +// 2) Make sure "OSReport EXI (OSREPORT)" log type is enabled +// 3) In the UI, make sure 'Show log' checkbox is checked in View menu +static void LogOverEXI(char* msg, int len) { + u32 cmd = 0x80000000 | (0x800400 << 6); // write flag, UART base address + + // https://hitmen.c02.at/files/yagcd/yagcd/chap10.html + // Try to acquire "MASK ROM"/"IPL" link + // Writing to the IPL is used for debug message logging + if (EXI_Lock(EXI_CHANNEL_0, EXI_DEVICE_1, NULL) == 0) return; + if (EXI_Select(EXI_CHANNEL_0, EXI_DEVICE_1, EXI_SPEED8MHZ) == 0) { + EXI_Unlock(EXI_CHANNEL_0); return; + } + + EXI_Imm( EXI_CHANNEL_0, &cmd, 4, EXI_WRITE, NULL); + EXI_Sync( EXI_CHANNEL_0); + EXI_ImmEx( EXI_CHANNEL_0, msg, len, EXI_WRITE); + EXI_Deselect(EXI_CHANNEL_0); + EXI_Unlock( EXI_CHANNEL_0); +} + +void Platform_Log(const char* msg, int len) { + char tmp[256 + 1]; + len = min(len, 256); + // See EXI_DeviceIPL.cpp in Dolphin, \r is what triggers buffered message to be logged + Mem_Copy(tmp, msg, len); tmp[len] = '\r'; + + LogOverEXI(tmp, len + 1); +} + +#define GCWII_EPOCH_ADJUST 946684800ULL // GameCube/Wii time epoch is year 2000, not 1970 + +TimeMS DateTime_CurrentUTC(void) { + u64 raw = gettime(); + u64 secs = ticks_to_secs(raw); + return secs + UNIX_EPOCH_SECONDS + GCWII_EPOCH_ADJUST; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct timeval cur; + struct tm loc_time; + gettimeofday(&cur, NULL); + localtime_r(&cur.tv_sec, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + +cc_uint64 Stopwatch_Measure(void) { + return gettime(); +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return ticks_to_microsecs(end - beg); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static char root_buffer[NATIVE_STR_LEN]; +static cc_string root_path = String_FromArray(root_buffer); + +static bool fat_available; +// trying to call mkdir etc with no FAT device loaded seems to be broken (dolphin crashes due to trying to execute invalid instruction) +// https://github.com/Patater/newlib/blob/8a9e3aaad59732842b08ad5fc19e0acf550a418a/libgloss/libsysbase/mkdir.c and +// https://github.com/Patater/newlib/blob/8a9e3aaad59732842b08ad5fc19e0acf550a418a/newlib/libc/include/sys/iosupport.h +// FindDevice() returns -1 when no matching device, however the code still unconditionally does "if (devoptab_list[dev]->mkdir_r) {" +// - so will either attempt to access or execute invalid memory + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + *str++ = '/'; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + if (!fat_available) return ENOSYS; + + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + return mkdir(str, 0) == -1 ? errno : 0; +} + +int File_Exists(const cc_string* path) { + if (!fat_available) return false; + + char str[NATIVE_STR_LEN]; + struct stat sb; + GetNativePath(str, path); + return stat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + if (!fat_available) return ENOSYS; + + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + struct dirent* entry; + int res; + + GetNativePath(str, dirPath); + DIR* dirPtr = opendir(str); + if (!dirPtr) return errno; + + // POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." + // errno is sometimes leftover from previous calls, so always reset it before readdir gets called + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + // ignore . and .. entry + char* src = entry->d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + int is_dir = entry->d_type == DT_DIR; + // TODO: fallback to stat when this fails + + if (is_dir) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; // return code from readdir + closedir(dirPtr); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + *file = open(str, mode, 0); + return *file == -1 ? errno : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + if (!fat_available) return ReturnCode_FileNotFound; + return File_Do(file, path, O_RDONLY); +} + +cc_result File_Create(cc_file* file, const cc_string* path) { + if (!fat_available) return ENOTSUP; + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +} + +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + if (!fat_available) return ENOTSUP; + return File_Do(file, path, O_RDWR | O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + *bytesRead = read(file, data, count); + return *bytesRead == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + *bytesWrote = write(file, data, count); + return *bytesWrote == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + return close(file) == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return lseek(file, offset, modes[seekType]) == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = lseek(file, 0, SEEK_CUR); + return *pos == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + struct stat st; + if (fstat(file, &st) == -1) { *len = -1; return errno; } + *len = st.st_size; return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { usleep(milliseconds * 1000); } + +static void* ExecThread(void* param) { + ((Thread_StartFunc)param)(); + return NULL; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + lwp_t* thread = (lwp_t*)Mem_Alloc(1, sizeof(lwp_t), "thread"); + *handle = thread; + + int res = LWP_CreateThread(thread, ExecThread, (void*)func, NULL, stackSize, 80); + if (res) Logger_Abort2(res, "Creating thread"); +} + +void Thread_Detach(void* handle) { + // TODO: Leaks return value of thread ??? + lwp_t* ptr = (lwp_t*)handle; + Mem_Free(ptr); +} + +void Thread_Join(void* handle) { + lwp_t* ptr = (lwp_t*)handle; + int res = LWP_JoinThread(*ptr, NULL); + if (res) Logger_Abort2(res, "Joining thread"); + Mem_Free(ptr); +} + +void* Mutex_Create(const char* name) { + mutex_t* ptr = (mutex_t*)Mem_Alloc(1, sizeof(mutex_t), "mutex"); + int res = LWP_MutexInit(ptr, false); + if (res) Logger_Abort2(res, "Creating mutex"); + return ptr; +} + +void Mutex_Free(void* handle) { + mutex_t* mutex = (mutex_t*)handle; + int res = LWP_MutexDestroy(*mutex); + if (res) Logger_Abort2(res, "Destroying mutex"); + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + mutex_t* mutex = (mutex_t*)handle; + int res = LWP_MutexLock(*mutex); + if (res) Logger_Abort2(res, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + mutex_t* mutex = (mutex_t*)handle; + int res = LWP_MutexUnlock(*mutex); + if (res) Logger_Abort2(res, "Unlocking mutex"); +} + +// should really use a semaphore with max 1.. too bad no 'TimedWait' though +struct WaitData { + cond_t cond; + mutex_t mutex; + int signalled; // For when Waitable_Signal is called before Waitable_Wait +}; + +void* Waitable_Create(const char* name) { + struct WaitData* ptr = (struct WaitData*)Mem_Alloc(1, sizeof(struct WaitData), "waitable"); + int res; + + res = LWP_CondInit(&ptr->cond); + if (res) Logger_Abort2(res, "Creating waitable"); + res = LWP_MutexInit(&ptr->mutex, false); + if (res) Logger_Abort2(res, "Creating waitable mutex"); + + ptr->signalled = false; + return ptr; +} + +void Waitable_Free(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + int res; + + res = LWP_CondDestroy(ptr->cond); + if (res) Logger_Abort2(res, "Destroying waitable"); + res = LWP_MutexDestroy(ptr->mutex); + if (res) Logger_Abort2(res, "Destroying waitable mutex"); + Mem_Free(handle); +} + +void Waitable_Signal(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + int res; + + Mutex_Lock(&ptr->mutex); + ptr->signalled = true; + Mutex_Unlock(&ptr->mutex); + + res = LWP_CondSignal(ptr->cond); + if (res) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + int res; + + Mutex_Lock(&ptr->mutex); + if (!ptr->signalled) { + res = LWP_CondWait(ptr->cond, ptr->mutex); + if (res) Logger_Abort2(res, "Waitable wait"); + } + ptr->signalled = false; + Mutex_Unlock(&ptr->mutex); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + struct WaitData* ptr = (struct WaitData*)handle; + struct timespec ts; + int res; + + ts.tv_sec = milliseconds / 1000; + ts.tv_nsec = milliseconds % 1000; + + Mutex_Lock(&ptr->mutex); + if (!ptr->signalled) { + res = LWP_CondTimedWait(ptr->cond, ptr->mutex, &ts); + if (res && res != ETIMEDOUT) Logger_Abort2(res, "Waitable wait for"); + } + ptr->signalled = false; + Mutex_Unlock(&ptr->mutex); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result ParseHost(const char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { +#ifdef HW_RVL + struct hostent* res = net_gethostbyname(host); + struct sockaddr_in* addr4; + char* src_addr; + int i; + + // avoid confusion with SSL error codes + // e.g. FFFF FFF7 > FF00 FFF7 + if (!res) return -0xFF0000 + errno; + + // Must have at least one IPv4 address + if (res->h_addrtype != AF_INET) return ERR_INVALID_ARGUMENT; + if (!res->h_addr_list) return ERR_INVALID_ARGUMENT; + + for (i = 0; i < SOCKET_MAX_ADDRS; i++) + { + src_addr = res->h_addr_list[i]; + if (!src_addr) break; + addrs[i].size = sizeof(struct sockaddr_in); + + addr4 = (struct sockaddr_in*)addrs[i].data; + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + addr4->sin_addr = *(struct in_addr*)src_addr; + } + + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +#else + // DNS resolution not implemented in gamecube libbba + return ERR_NOT_SUPPORTED; +#endif +} +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, address); + *numValidAddrs = 1; + if (inet_aton(str, &addr4->sin_addr) > 0) { + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(*addr4); + return 0; + } + + return ParseHost(str, port, addrs, numValidAddrs); +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + + *s = net_socket(raw->sa_family, SOCK_STREAM, 0); + if (*s < 0) return *s; + + if (nonblocking) { + int blocking_raw = -1; /* non-blocking mode */ + net_ioctl(*s, FIONBIO, &blocking_raw); + } + + res = net_connect(*s, raw, addr->size); + return res < 0 ? res : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int res = net_recv(s, data, count, 0); + if (res < 0) { *modified = 0; return res; } + + *modified = res; return 0; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int res = net_send(s, data, count, 0); + if (res < 0) { *modified = 0; return res; } + + *modified = res; return 0; +} + +void Socket_Close(cc_socket s) { + net_shutdown(s, 2); // SHUT_RDWR = 2 + net_close(s); +} + +#ifdef HW_RVL +// libogc only implements net_poll for wii currently +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollsd pfd; + pfd.socket = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + + int res = net_poll(&pfd, 1, 0); + if (res < 0) { *success = false; return res; } + + // to match select, closed socket still counts as readable + int flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} +#else +// libogc only implements net_select for gamecube currently +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + fd_set set; + struct timeval time = { 0 }; + int res; // number of 'ready' sockets + FD_ZERO(&set); + FD_SET(s, &set); + if (mode == SOCKET_POLL_READ) { + res = net_select(s + 1, &set, NULL, NULL, &time); + } else { + res = net_select(s + 1, NULL, &set, NULL, &time); + } + if (res < 0) { *success = false; return res; } + *success = FD_ISSET(s, &set) != 0; return 0; +} +#endif + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + u32 resultSize = sizeof(u32); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + return 0; + // TODO FIX with updated devkitpro ??? + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + net_getsockopt(s, SOL_SOCKET, SO_ERROR, &res, resultSize); + return res; +} +static void InitSockets(void) { +#ifdef HW_RVL + int ret = net_init(); + Platform_Log1("Network setup result: %i", &ret); +#else + // https://github.com/devkitPro/wii-examples/blob/master/devices/network/sockettest/source/sockettest.c + char localip[16] = {0}; + char netmask[16] = {0}; + char gateway[16] = {0}; + + int ret = if_config(localip, netmask, gateway, TRUE, 20); + if (ret >= 0) { + Platform_Log3("Network ip: %c, gw: %c, mask %c", localip, gateway, netmask); + } else { + Platform_Log1("Network setup failed: %i", &ret); + } +#endif +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static void AppendDevice(cc_string* path, char* cwd) { + // try to find device FAT mounted on, otherwise default to SD card + if (!cwd) { String_AppendConst(path, "sd"); return; } + + Platform_Log1("CWD: %c", cwd); + cc_string cwd_ = String_FromReadonly(cwd); + int deviceEnd = String_IndexOf(&cwd_, ':'); + + if (deviceEnd >= 0) { + // e.g. "card0:/" becomes "card0" + String_AppendAll(path, cwd, deviceEnd); + } else { + String_AppendConst(path, "sd"); + } +} + +static void FindRootDirectory(void) { + char cwdBuffer[NATIVE_STR_LEN] = { 0 }; + char* cwd = getcwd(cwdBuffer, NATIVE_STR_LEN); + + root_path.length = 0; + AppendDevice(&root_path, cwd); + String_AppendConst(&root_path, ":/ClassiCube"); +} + +static void CreateRootDirectory(void) { + if (!fat_available) return; + root_buffer[root_path.length] = '\0'; + + // irectory_Create(&String_Empty); just returns error 20 + int res = mkdir(root_buffer, 0); + int err = res == -1 ? errno : 0; + Platform_Log1("Created root directory: %i", &err); +} + +void Platform_Init(void) { + fat_available = fatInitDefault(); + Platform_ReadonlyFilesystem = !fat_available; + + FindRootDirectory(); + CreateRootDirectory(); + + InitSockets(); +} +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +#if defined HW_RVL +#include + +static cc_result GetMachineID(cc_uint32* key) { + return ES_GetDeviceID(key); +} +#else +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif + +#endif diff --git a/src/Platform_MacClassic.c b/src/Platform_MacClassic.c new file mode 100644 index 0000000..d74364d --- /dev/null +++ b/src/Platform_MacClassic.c @@ -0,0 +1,544 @@ +#include "Core.h" +#if defined CC_BUILD_MACCLASSIC + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "SystemFonts.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" +#include +#include + +#undef true +#undef false +#include +#include +#include +#include +#include + +const cc_result ReturnCode_FileShareViolation = 1000000000; +const cc_result ReturnCode_FileNotFound = fnfErr; +const cc_result ReturnCode_SocketInProgess = 1000000; +const cc_result ReturnCode_SocketWouldBlock = 1000000; +const cc_result ReturnCode_DirectoryExists = dupFNErr; + +#if TARGET_CPU_68K +const char* Platform_AppNameSuffix = " MAC 68k"; +#else +const char* Platform_AppNameSuffix = " MAC PPC"; +#endif +cc_bool Platform_SingleProcess = true; +static long sysVersion; + + +/*########################################################################################################################* +*---------------------------------------------------Imported headers------------------------------------------------------* +*#########################################################################################################################*/ +// On 68k these are implemented using direct 68k opcodes +// On PPC these are implemented using function calls +#if TARGET_CPU_68K + #define MAC_SYSAPI(_type) static _type + #define MAC_ONEWORDINLINE(w1) = w1 + #define MAC_TWOWORDINLINE(w1,w2) = {w1, w2} + #define MAC_THREEWORDINLINE(w1,w2,w3) = {w1, w2, w3} + #define MAC_FOURWORDINLINE(w1,w2,w3,w4) = {w1, w2, w3, w4} +#else + #define MAC_SYSAPI(_type) extern pascal _type + #define MAC_ONEWORDINLINE(w1) + #define MAC_TWOWORDINLINE(w1,w2) + #define MAC_THREEWORDINLINE(w1,w2,w3) + #define MAC_FOURWORDINLINE(w1,w2,w3,w4) +#endif +typedef unsigned long MAC_FourCharCode; +static const int MAC_smSystemScript = -1; + +// ==================================== IMPORTS FROM TIMER.H ==================================== +// Availability: in InterfaceLib 7.1 and later +MAC_SYSAPI(void) Microseconds(cc_uint64* microTickCount) MAC_FOURWORDINLINE(0xA193, 0x225F, 0x22C8, 0x2280); + +/*########################################################################################################################* +*---------------------------------------------------------Memory----------------------------------------------------------* +*#########################################################################################################################*/ +void* Mem_Set(void* dst, cc_uint8 value, unsigned numBytes) { return memset( dst, value, numBytes); } +void* Mem_Copy(void* dst, const void* src, unsigned numBytes) { return memcpy( dst, src, numBytes); } +void* Mem_Move(void* dst, const void* src, unsigned numBytes) { return memmove(dst, src, numBytes); } + +void* Mem_TryAlloc(cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? NewPtr(size) : NULL; +} + +void* Mem_TryAllocCleared(cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? NewPtrClear(size) : NULL; +} + +void* Mem_TryRealloc(void* mem, cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + if (!size) return NULL; + if (!mem) return NewPtr(size); + + // Try to resize in place + MemError(); + SetPtrSize(mem, size); + if (!MemError()) return mem; + + void* newMem = NewPtr(size); + if (!newMem) return NULL; + + Mem_Copy(newMem, mem, GetPtrSize(mem)); + return newMem; +} + +void Mem_Free(void* mem) { + if (mem) DisposePtr(mem); +} + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Console_Write(const char* msg, int len); + +void Platform_Log(const char* msg, int len) { + Console_Write(msg, len); +} + +// classic macOS uses an epoch of 1904 +#define EPOCH_ADJUSTMENT 2082866400UL + +static time_t gettod(void) { + unsigned long secs; + GetDateTime(&secs); + return secs - EPOCH_ADJUSTMENT; +} + +TimeMS DateTime_CurrentUTC(void) { + time_t secs = gettod(); + return (cc_uint64)secs + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct tm loc_time; + time_t secs = gettod(); + localtime_r(&secs, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +#define MS_PER_SEC 1000000ULL + +cc_uint64 Stopwatch_Measure(void) { + cc_uint64 count; + if (sysVersion < 0x7000) { + // 60 ticks a second + count = TickCount(); + return count * MS_PER_SEC / 60; + } + + Microseconds(&count); + return count; +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return end - beg; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static int retrievedWD, wd_refNum, wd_dirID; + +static void GetNativePath(char* dst, const cc_string* src) { + char* str = dst; + str++; // placeholder for length later + *str++ = ':'; + + // Classic Mac OS uses : to separate directories + for (int i = 0; i < src->length; i++) + { + char c = (char)src->buffer[i]; + if (c == '/') c = ':'; + *str++ = c; + } + *str = '\0'; + dst[0] = String_Length(dst + 1); // pascal strings + + if (retrievedWD) return; + retrievedWD = true; + + WDPBRec r = { 0 }; + PBHGetVolSync(&r); + wd_refNum = r.ioWDVRefNum; + wd_dirID = r.ioWDDirID; + + int V = r.ioWDVRefNum, D = r.ioWDDirID; + Platform_Log2("Working directory: %i, %i", &V, &D); +} + +static int DoOpenDF(char* name, char perm, cc_file* file) { + HParamBlockRec pb; + Mem_Set(&pb, 0, sizeof(pb)); + + pb.fileParam.ioVRefNum = wd_refNum; + pb.fileParam.ioDirID = wd_dirID; + pb.fileParam.ioNamePtr = name; + pb.ioParam.ioPermssn = perm; + + int err = PBHOpenDFSync(&pb); + *file = pb.ioParam.ioRefNum; + return err; +} + +static int DoCreateFile(char* name) { + HParamBlockRec pb; + Mem_Set(&pb, 0, sizeof(pb)); + + pb.fileParam.ioVRefNum = wd_refNum; + pb.fileParam.ioDirID = wd_dirID; + pb.fileParam.ioNamePtr = name; + + return PBHCreateSync(&pb); +} + +static int DoCreateFolder(char* name) { + HParamBlockRec pb; + Mem_Set(&pb, 0, sizeof(pb)); + + pb.fileParam.ioVRefNum = wd_refNum; + pb.fileParam.ioDirID = wd_dirID; + pb.fileParam.ioNamePtr = name; + + return PBDirCreateSync(&pb); +} + + +void Directory_GetCachePath(cc_string* path) { } + +cc_result Directory_Create(const cc_string* path) { + char buffer[NATIVE_STR_LEN]; + GetNativePath(buffer, path); + return DoCreateFolder(buffer); +} + +int File_Exists(const cc_string* path) { + char buffer[NATIVE_STR_LEN]; + GetNativePath(buffer, path); + + return 0; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + char buffer[NATIVE_STR_LEN]; + GetNativePath(buffer, path); + + return DoOpenDF(buffer, fsRdPerm, file); +} + +cc_result File_Create(cc_file* file, const cc_string* path) { + char buffer[NATIVE_STR_LEN]; + GetNativePath(buffer, path); + + int res = DoCreateFile(buffer); + if (res && res != dupFNErr) return res; + + return DoOpenDF(buffer, fsWrPerm, file); +} + +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + char buffer[NATIVE_STR_LEN]; + GetNativePath(buffer, path); + + int res = DoCreateFile(buffer); + if (res && res != dupFNErr) return res; + + return DoOpenDF(buffer, fsRdWrPerm, file); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + ParamBlockRec pb; + pb.ioParam.ioRefNum = file; + pb.ioParam.ioBuffer = data; + pb.ioParam.ioReqCount = count; + pb.ioParam.ioPosMode = fsAtMark; + + int err = PBReadSync(&pb); + *bytesRead = pb.ioParam.ioActCount; + return err; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + ParamBlockRec pb; + pb.ioParam.ioRefNum = file; + pb.ioParam.ioBuffer = data; + pb.ioParam.ioReqCount = count; + pb.ioParam.ioPosMode = fsAtMark; + + int err = PBWriteSync(&pb); + *bytesWrote = pb.ioParam.ioActCount; + return err; +} + +cc_result File_Close(cc_file file) { + ParamBlockRec pb; + pb.ioParam.ioRefNum = file; + + return PBCloseSync(&pb); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[] = { fsFromStart, fsFromMark, fsFromLEOF }; + ParamBlockRec pb; + pb.ioParam.ioRefNum = file; + pb.ioParam.ioPosMode = modes[seekType]; + pb.ioParam.ioPosOffset = offset; + + return PBSetFPosSync(&pb); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + ParamBlockRec pb; + pb.ioParam.ioRefNum = file; + + int err = PBGetFPosSync(&pb); + *pos = pb.ioParam.ioPosOffset; + return err; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + ParamBlockRec pb; + pb.ioParam.ioRefNum = file; + + int err = PBGetEOFSync(&pb); + *len = (cc_uint32)pb.ioParam.ioMisc; + return err; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + long delay = milliseconds * 1000 / 60; + long final; + Delay(delay, &final); +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + *handle = NULL; + // TODO +} + +void Thread_Detach(void* handle) { + // TODO +} + +void Thread_Join(void* handle) { + // TODO +} + +void* Mutex_Create(const char* name) { + return NULL; +} + +void Mutex_Free(void* handle) { + // TODO +} + +void Mutex_Lock(void* handle) { + // TODO +} + +void Mutex_Unlock(void* handle) { + // TODO +} + +void* Waitable_Create(const char* name) { + return NULL; +} + +void Waitable_Free(void* handle) { + // TODO +} + +void Waitable_Signal(void* handle) { + // TODO +} + +void Waitable_Wait(void* handle) { + // TODO +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + // TODO +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +void Socket_Close(cc_socket s) { + +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Process/Module------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Process_OpenSupported = false; +static char gameArgs[GAME_MAX_CMDARGS][STRING_SIZE]; +static int gameNumArgs; + +int Platform_GetCommandLineArgs(int argc, STRING_REF char** argv, cc_string* args) { + int count = gameNumArgs; + for (int i = 0; i < count; i++) + { + args[i] = String_FromRawArray(gameArgs[i]); + } + + // clear arguments so after game is closed, launcher is started + gameNumArgs = 0; + return count; +} + +cc_result Platform_SetDefaultCurrentDirectory(int argc, char **argv) { + return 0; +} + +cc_result Process_StartGame2(const cc_string* args, int numArgs) { + for (int i = 0; i < numArgs; i++) + { + String_CopyToRawArray(gameArgs[i], &args[i]); + } + + gameNumArgs = numArgs; + return 0; +} + +void Process_Exit(cc_result code) { + ExitToShell(); + for(;;) { } +} + +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Updater----------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Updater_Supported = false; +cc_bool Updater_Clean(void) { return true; } + +const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; + +cc_result Updater_Start(const char** action) { + return ERR_NOT_SUPPORTED; +} + +cc_result Updater_GetBuildTime(cc_uint64* timestamp) { + return ERR_NOT_SUPPORTED; +} + +cc_result Updater_MarkExecutable(void) { + return ERR_NOT_SUPPORTED; +} + +cc_result Updater_SetNewBuildTime(cc_uint64 timestamp) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Dynamic lib-------------------------------------------------------* +*#########################################################################################################################*/ +const cc_string DynamicLib_Ext = String_FromConst(".dylib"); + +void* DynamicLib_Load2(const cc_string* path) { + return NULL; +} + +void* DynamicLib_Get2(void* lib, const char* name) { + return NULL; +} + +cc_bool DynamicLib_DescribeError(cc_string* dst) { + return false; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + // TODO + return false; +} + +void Platform_Init(void) { + Gestalt(gestaltSystemVersion, &sysVersion); + Platform_Log1("Running on Mac OS %h", &sysVersion); + + cc_string path = String_FromConst("aB.txt"); + char buffer[NATIVE_STR_LEN]; + GetNativePath(buffer, &path); + + int ERR2 = DoCreateFile(buffer); + Platform_Log1("TEST FILE: %i", &ERR2); +} + +cc_result Platform_Encrypt(const void* data, int len, cc_string* dst) { + return ERR_NOT_SUPPORTED; +} + +cc_result Platform_Decrypt(const void* data, int len, cc_string* dst) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_N64.c b/src/Platform_N64.c new file mode 100644 index 0000000..0d26b4f --- /dev/null +++ b/src/Platform_N64.c @@ -0,0 +1,298 @@ +#include "Core.h" +#if defined CC_BUILD_N64 +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "Options.h" + +#include +#include +#include +#include +#include +//#include +//#include +//#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " N64"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + + cc_uint64 delta = end - beg; + return TIMER_MICROS_LL(delta); +} + +cc_uint64 Stopwatch_Measure(void) { + return timer_ticks(); +} + +void Platform_Log(const char* msg, int len) { + write(STDERR_FILENO, msg, len); + write(STDERR_FILENO, "\n", 1); +} + +TimeMS DateTime_CurrentUTC(void) { + return 0; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + rtc_time_t curTime = { 0 }; + rtc_get(&curTime); + + t->year = curTime.year; + t->month = curTime.month; + t->day = curTime.day; + t->hour = curTime.hour; + t->minute = curTime.min; + t->second = curTime.sec; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("/"); + +static void GetNativePath(char* str, const cc_string* path) { + // TODO temp hack + cc_string path_ = *path; + int idx = String_IndexOf(path, '/'); + if (idx >= 0) path_ = String_UNSAFE_SubstringAt(&path_, idx + 1); + + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, &path_); +} + +cc_result Directory_Create(const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +int File_Exists(const cc_string* path) { + return false; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + return ERR_NOT_SUPPORTED; // TODO add support +} + +static cc_result File_Do(cc_file* file, const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + //*file = -1; + //return ReturnCode_FileNotFound; + // TODO: Why does trying this code break everything + + int ret = dfs_open(str); + Platform_Log2("Opened %c = %i", str, &ret); + if (ret < 0) { *file = -1; return ret; } + + *file = ret; + return 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + *file = -1; + return ERR_NOT_SUPPORTED; + //return File_Do(file, path); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + *file = -1; + return ERR_NOT_SUPPORTED; + //return File_Do(file, path); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + int ret = dfs_read(data, 1, count, file); + if (ret < 0) { *bytesRead = -1; return ret; } + + *bytesRead = ret; + return 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Close(cc_file file) { + if (file < 0) return 0; + return dfs_close(file); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return dfs_seek(file, offset, modes[seekType]); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + int ret = dfs_tell(file); + if (ret < 0) { *pos = -1; return ret; } + + *pos = ret; + return 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + int ret = dfs_size(file); + if (ret < 0) { *len = -1; return ret; } + + *len = ret; + return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +// !!! NOTE: PSP uses cooperative multithreading (not preemptive multithreading) !!! +void Thread_Sleep(cc_uint32 milliseconds) { + wait_ms(milliseconds); +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + *handle = NULL; +} + +void Thread_Detach(void* handle) { +} + +void Thread_Join(void* handle) { +} + +void* Mutex_Create(const char* name) { + return NULL; +} + +void Mutex_Free(void* handle) { +} + +void Mutex_Lock(void* handle) { +} + +void Mutex_Unlock(void* handle) { +} + +void* Waitable_Create(const char* name) { + return NULL; +} + +void Waitable_Free(void* handle) { +} + +void Waitable_Signal(void* handle) { +} + +void Waitable_Wait(void* handle) { +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +void Socket_Close(cc_socket s) { } + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +// See src/n64sys.c +static void DisableFpuExceptions(void) { + uint32_t fcr31 = C1_FCR31(); + + fcr31 &= ~(C1_CAUSE_OVERFLOW | C1_CAUSE_UNDERFLOW | C1_CAUSE_NOT_IMPLEMENTED | C1_CAUSE_INEXACT_OP); + fcr31 |= C1_ENABLE_DIV_BY_0 | C1_ENABLE_INVALID_OP; + fcr31 |= C1_FCR31_FS; + + C1_WRITE_FCR31(fcr31); +} + +void Platform_Init(void) { + debug_init_isviewer(); + debug_init_usblog(); + DisableFpuExceptions(); + + Platform_ReadonlyFilesystem = true; + dfs_init(DFS_DEFAULT_LOCATION); + timer_init(); + rtc_init(); +} +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_NDS.c b/src/Platform_NDS.c new file mode 100644 index 0000000..496722a --- /dev/null +++ b/src/Platform_NDS.c @@ -0,0 +1,501 @@ +#include "Core.h" +#if defined CC_BUILD_NDS +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "Options.h" +#include "Animations.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " NDS"; +extern cc_bool keyboardOpen; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +static u32 last_raw; +static u64 base_time; + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + + return timerTicks2usec(end - beg); +} + +cc_uint64 Stopwatch_Measure(void) { + u32 raw = cpuGetTiming(); + // Since counter is only a 32 bit integer, it overflows after a minute or two + if (last_raw > 0xF0000000 && raw < 0x10000000) { + base_time += 0x100000000ULL; + } + + last_raw = raw; + return base_time + raw; +} + +static void LogNocash(const char* msg, int len) { + // Can only be up to 120 bytes total + char buffer[120]; + len = min(len, 118); + + Mem_Copy(buffer, msg, len); + buffer[len + 0] = '\n'; + buffer[len + 1] = '\0'; + nocashWrite(buffer, len + 2); +} + +extern void consolePrintString(const char* ptr, int len); +void Platform_Log(const char* msg, int len) { + LogNocash(msg, len); + consolePrintString(msg, len); +} + +TimeMS DateTime_CurrentUTC(void) { + struct timeval cur; + gettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct timeval cur; + struct tm loc_time; + gettimeofday(&cur, NULL); + localtime_r(&cur.tv_sec, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static cc_string root_path = String_FromConst("fat:/"); // may be overriden in InitFilesystem +static bool fat_available; + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + if (!fat_available) return 0; + + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + Platform_Log1("mkdir %c", str); + + return mkdir(str, 0) == -1 ? errno : 0; +} + +int File_Exists(const cc_string* path) { + if (!fat_available) return false; + + char str[NATIVE_STR_LEN]; + struct stat sb; + GetNativePath(str, path); + Platform_Log1("Check %c", str); + + return stat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + struct dirent* entry; + int res; + + String_EncodeUtf8(str, dirPath); + DIR* dirPtr = opendir(str); + if (!dirPtr) return errno; + + /* POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." */ + /* errno is sometimes leftover from previous calls, so always reset it before readdir gets called */ + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + /* ignore . and .. entry */ + char* src = entry->d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + + if (entry->d_type == DT_DIR) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; /* return code from readdir */ + closedir(dirPtr); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode, const char* type) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + Platform_Log2("%c %c", type, str); + + *file = open(str, mode, 0); + return *file == -1 ? errno : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + if (!fat_available) return ReturnCode_FileNotFound; + return File_Do(file, path, O_RDONLY, "Open"); +} + +cc_result File_Create(cc_file* file, const cc_string* path) { + if (!fat_available) return ENOTSUP; + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC, "Create"); +} + +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + if (!fat_available) return ENOTSUP; + return File_Do(file, path, O_RDWR | O_CREAT, "Update"); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + *bytesRead = read(file, data, count); + return *bytesRead == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + *bytesWrote = write(file, data, count); + return *bytesWrote == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + return close(file) == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return lseek(file, offset, modes[seekType]) == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = lseek(file, 0, SEEK_CUR); + return *pos == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + struct stat st; + if (fstat(file, &st) == -1) { *len = -1; return errno; } + *len = st.st_size; return 0; +} + +static int LoadFatFilesystem(void* arg) { + fat_available = fatInitDefault(); + return 0; +} + +static void InitFilesystem(void) { + cothread_t thread = cothread_create(LoadFatFilesystem, NULL, 0, 0); + // If running with DSi mode in melonDS and the internal SD card is enabled, then + // fatInitDefault gets stuck in sdmmc_ReadSectors - because the fifoWaitValue32Async will never return + // (You can tell when this happens - "MMC: unknown CMD 17 00000000" is logged to console) + // However, since it does yield to cothreads, workaround this by running fatInitDefault on another thread + // and then giving up if it takes too long.. not the most elegant solution, but it does work + if (thread == -1) { + LoadFatFilesystem(NULL); + } else { + for (int i = 0; i < 100; i++) + { + cothread_yield(); + if (cothread_has_joined(thread)) break; + + swiDelay(2000); + } + } + + char* dir = fatGetDefaultCwd(); + if (dir) { + Platform_Log1("CWD: %c", dir); + root_path.buffer = dir; + root_path.length = String_Length(dir); + } + Platform_ReadonlyFilesystem = !fat_available; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +// !!! NOTE: PSP uses cooperative multithreading (not preemptive multithreading) !!! +void Thread_Sleep(cc_uint32 milliseconds) { + swiDelay(8378 * milliseconds); // TODO probably wrong +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + *handle = NULL; +} + +void Thread_Detach(void* handle) { +} + +void Thread_Join(void* handle) { +} + +void* Mutex_Create(const char* name) { + return NULL; +} + +void Mutex_Free(void* handle) { +} + +void Mutex_Lock(void* handle) { +} + +void Mutex_Unlock(void* handle) { +} + +void* Waitable_Create(const char* name) { + return NULL; +} + +void Waitable_Free(void* handle) { +} + +void Waitable_Signal(void* handle) { +} + +void Waitable_Wait(void* handle) { +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool net_supported = true; + +static cc_result ParseHost(const char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct hostent* res = gethostbyname(host); + struct sockaddr_in* addr4; + char* src_addr; + int i; + + // avoid confusion with SSL error codes + // e.g. FFFF FFF7 > FF00 FFF7 + if (!res) return -0xFF0000 + errno; + + // Must have at least one IPv4 address + if (res->h_addrtype != AF_INET) return ERR_INVALID_ARGUMENT; + if (!res->h_addr_list) return ERR_INVALID_ARGUMENT; + + for (i = 0; i < SOCKET_MAX_ADDRS; i++) + { + src_addr = res->h_addr_list[i]; + if (!src_addr) break; + addrs[i].size = sizeof(struct sockaddr_in); + + addr4 = (struct sockaddr_in*)addrs[i].data; + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + addr4->sin_addr = *(struct in_addr*)src_addr; + } + + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, address); + + if (!net_supported) return ERR_NOT_SUPPORTED; + *numValidAddrs = 1; + + if (inet_aton(str, &addr4->sin_addr) > 0) { + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(*addr4); + return 0; + } + + return ParseHost(str, port, addrs, numValidAddrs); +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + if (!net_supported) { *s = -1; return ERR_NOT_SUPPORTED; } + + *s = socket(raw->sa_family, SOCK_STREAM, 0); + if (*s < 0) return errno; + + if (nonblocking) { + int blocking_raw = 1; /* non-blocking mode */ + ioctl(*s, FIONBIO, &blocking_raw); + } + + res = connect(*s, raw, addr->size); + return res < 0 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int res = recv(s, data, count, 0); + if (res < 0) { *modified = 0; return errno; } + + *modified = res; return 0; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int res = send(s, data, count, 0); + if (res < 0) { *modified = 0; return errno; } + + *modified = res; return 0; +} + +void Socket_Close(cc_socket s) { + shutdown(s, 2); // SHUT_RDWR = 2 + closesocket(s); +} + +// libogc only implements net_select for gamecube currently +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + fd_set set; + struct timeval time = { 0 }; + int res; // number of 'ready' sockets + FD_ZERO(&set); + FD_SET(s, &set); + if (mode == SOCKET_POLL_READ) { + res = select(s + 1, &set, NULL, NULL, &time); + } else { + res = select(s + 1, NULL, &set, NULL, &time); + } + if (res < 0) { *success = false; return errno; } + *success = FD_ISSET(s, &set) != 0; return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + int resultSize = sizeof(int); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + +static void InitNetworking(void) { + if (!Wifi_InitDefault(INIT_ONLY)) { + Platform_LogConst("Initing WIFI failed"); + net_supported = false; return; + } + Wifi_AutoConnect(); + + for (int i = 0; i < 300; i++) + { + int status = Wifi_AssocStatus(); + if (status == ASSOCSTATUS_ASSOCIATED) return; + + if (status == ASSOCSTATUS_CANNOTCONNECT) { + Platform_LogConst("Can't connect to WIFI"); + net_supported = false; return; + } + swiWaitForVBlank(); + } + Platform_LogConst("Gave up after 300 tries"); + net_supported = false; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Init(void) { + cc_bool dsiMode = isDSiMode(); + Platform_Log1("Running in %c mode", dsiMode ? "DSi" : "DS"); + + InitFilesystem(); + InitNetworking(); + cpuStartTiming(1); +} +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_PS1.c b/src/Platform_PS1.c new file mode 100644 index 0000000..02316d5 --- /dev/null +++ b/src/Platform_PS1.c @@ -0,0 +1,250 @@ +#include "Core.h" +#if defined PLAT_PS1 + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "Options.h" +#include "PackedCol.h" +#include +#include +#include +#include +#include +#include +#include +void exit(int code) { _boot(); } + +// The SDK calloc doesn't zero memory, so need to override it +void* calloc(size_t num, size_t size) { + void* ptr = malloc(num * size); + if (ptr) memset(ptr, 0, num * size); + return ptr; +} +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = 99999; +const cc_result ReturnCode_DirectoryExists = 99999; + +const cc_result ReturnCode_SocketInProgess = -1; +const cc_result ReturnCode_SocketWouldBlock = -1; +const char* Platform_AppNameSuffix = " PS1"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Log(const char* msg, int len) { + char tmp[2048 + 1]; + len = min(len, 2048); + Mem_Copy(tmp, msg, len); tmp[len] = '\0'; + + printf("%s\n", tmp); +} + +TimeMS DateTime_CurrentUTC(void) { + return 0; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + Mem_Set(t, 0, sizeof(struct DateTime)); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +static volatile cc_uint32 irq_count; + +cc_uint64 Stopwatch_Measure(void) { + return irq_count; +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return (end - beg) * 1000; +} + +static void timer2_handler(void) { irq_count++; } + +static void Stopwatch_Init(void) { + TIMER_CTRL(2) = 0x0258; // CLK/8 input, IRQ on reload + TIMER_RELOAD(2) = (F_CPU / 8) / 1000; // 1000 Hz + + EnterCriticalSection(); + InterruptCallback(IRQ_TIMER2, &timer2_handler); + ExitCriticalSection(); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("cdrom:/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +int File_Exists(const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Create(cc_file* file, const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Close(cc_file file) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + // TODO sleep a bit + VSync(0); +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + *handle = NULL; +} + +void Thread_Detach(void* handle) { +} + +void Thread_Join(void* handle) { +} + +void* Mutex_Create(const char* name) { + return NULL; +} + +void Mutex_Free(void* handle) { +} + +void Mutex_Lock(void* handle) { +} + +void Mutex_Unlock(void* handle) { +} + +void* Waitable_Create(const char* name) { + return NULL; +} + +void Waitable_Free(void* handle) { +} + +void Waitable_Signal(void* handle) { +} + +void Waitable_Wait(void* handle) { +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +void Socket_Close(cc_socket s) { +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Init(void) { + ResetGraph(0); + Stopwatch_Init(); +} + +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_PS2.c b/src/Platform_PS2.c new file mode 100644 index 0000000..af62e87 --- /dev/null +++ b/src/Platform_PS2.c @@ -0,0 +1,763 @@ +#include "Core.h" +#if defined PLAT_PS2 + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define NEWLIB_PORT_AWARE +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = -4; +const cc_result ReturnCode_DirectoryExists = -8; + +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const char* Platform_AppNameSuffix = " PS2"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +static cc_bool hookedDebug; + +void Platform_Log(const char* msg, int len) { + char tmp[2048 + 1]; + len = min(len, 2048); + Mem_Copy(tmp, msg, len); tmp[len] = '\0'; + _print("%s", tmp); + +#ifdef PS2_DEBUG + volatile char* dst = (char*)0x1000F180; + + for (int i = 0; i < len; i++) + { + *dst = msg[i]; + } + *dst = '\n'; + *dst = '\r'; +#endif +} + +TimeMS DateTime_CurrentUTC(void) { + struct timeval cur; + gettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct timeval cur; + struct tm loc_time; + gettimeofday(&cur, NULL); + localtime_r(&cur.tv_sec, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +#define US_PER_SEC 1000000ULL + +cc_uint64 Stopwatch_Measure(void) { + return GetTimerSystemTime(); +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return (end - beg) * US_PER_SEC / kBUSCLK; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("mass:/ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + return fioMkdir(str); +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + io_stat_t sb; + GetNativePath(str, path); + return fioGetstat(str, &sb) >= 0 && (sb.mode & FIO_SO_IFREG); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + return ERR_NOT_SUPPORTED; + /*cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + struct dirent* entry; + int res; + + GetNativePath(str, dirPath); + DIR* dirPtr = opendir(str); + if (!dirPtr) return errno; + + // POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." + // errno is sometimes leftover from previous calls, so always reset it before readdir gets called + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + // ignore . and .. entry + char* src = entry->d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + int is_dir = entry->d_type == DT_DIR; + // TODO: fallback to stat when this fails + + if (is_dir) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; // return code from readdir + closedir(dirPtr); + return res;*/ +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + int res = fioOpen(str, mode); + *file = res; + return res < 0 ? res : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, FIO_O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, FIO_O_RDWR | FIO_O_CREAT | FIO_O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, FIO_O_RDWR | FIO_O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + int res = fioRead(file, data, count); + *bytesRead = res; + return res < 0 ? res : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + int res = fioWrite(file, data, count); + *bytesWrote = res; + return res < 0 ? res : 0; +} + +cc_result File_Close(cc_file file) { + return fioClose(file); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + + int res = fioLseek(file, offset, modes[seekType]); + return res < 0 ? res : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + int res = fioLseek(file, 0, SEEK_CUR); + *pos = res; + return res < 0 ? res : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + int cur_pos = fioLseek(file, 0, SEEK_CUR); + if (cur_pos < 0) return cur_pos; // error occurred + + // get end and then restore position + int res = fioLseek(file, 0, SEEK_END); + fioLseek(file, cur_pos, SEEK_SET); + + *len = res; + return res < 0 ? res : 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + DelayThread(milliseconds * 1000); +} + +static int ExecThread(void* param) { + ((Thread_StartFunc)param)(); + + int thdID = GetThreadId(); + ee_thread_status_t info; + + int res = ReferThreadStatus(thdID, &info); + if (res > 0 && info.stack) Mem_Free(info.stack); // TODO is it okay to free stack of running thread ???? + + return 0; // TODO detach ? +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + ee_thread_t thread = { 0 }; + thread.func = ExecThread; + thread.stack = Mem_Alloc(stackSize, 1, "Thread stack"); + thread.stack_size = stackSize; + thread.gp_reg = &_gp; + thread.initial_priority = 18; + + int thdID = CreateThread(&thread); + if (thdID < 0) Logger_Abort2(thdID, "Creating thread"); + *handle = (void*)thdID; + + int res = StartThread(thdID, (void*)func); + if (res < 0) Logger_Abort2(res, "Running thread"); +} + +void Thread_Detach(void* handle) { + // TODO do something +} + +void Thread_Join(void* handle) { + int thdID = (int)handle; + ee_thread_status_t info; + + for (;;) + { + int res = ReferThreadStatus(thdID, &info); + if (res) Logger_Abort("Checking thread status"); + + if (info.status == THS_DORMANT) break; + Thread_Sleep(10); // TODO better solution + } +} + +void* Mutex_Create(const char* name) { + ee_sema_t sema = { 0 }; + sema.init_count = 1; + sema.max_count = 1; + + int semID = CreateSema(&sema); + if (semID < 0) Logger_Abort2(semID, "Creating mutex"); + return (void*)semID; +} + +void Mutex_Free(void* handle) { + int semID = (int)handle; + int res = DeleteSema(semID); + + if (res) Logger_Abort2(res, "Destroying mutex"); +} + +void Mutex_Lock(void* handle) { + int semID = (int)handle; + int res = WaitSema(semID); + + if (res < 0) Logger_Abort2(res, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + int semID = (int)handle; + int res = SignalSema(semID); + + if (res < 0) Logger_Abort2(res, "Unlocking mutex"); +} + +void* Waitable_Create(const char* name) { + ee_sema_t sema = { 0 }; + sema.init_count = 0; + sema.max_count = 1; + + int semID = CreateSema(&sema); + if (semID < 0) Logger_Abort2(semID, "Creating waitable"); + return (void*)semID; +} + +void Waitable_Free(void* handle) { + int semID = (int)handle; + int res = DeleteSema(semID); + + if (res) Logger_Abort2(res, "Destroying waitable"); +} + +void Waitable_Signal(void* handle) { + int semID = (int)handle; + int res = SignalSema(semID); + + if (res < 0) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + int semID = (int)handle; + int res = WaitSema(semID); + + if (res < 0) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + Logger_Abort("Can't wait for"); + // TODO implement support +} + + +/*########################################################################################################################* +*-------------------------------------------------------Networking--------------------------------------------------------* +*#########################################################################################################################*/ +// https://github.com/ps2dev/ps2sdk/blob/master/NETMAN.txt +// https://github.com/ps2dev/ps2sdk/blob/master/ee/network/tcpip/samples/tcpip_dhcp/ps2ip.c +extern unsigned char DEV9_irx[]; +extern unsigned int size_DEV9_irx; + +extern unsigned char SMAP_irx[]; +extern unsigned int size_SMAP_irx; + +extern unsigned char NETMAN_irx[]; +extern unsigned int size_NETMAN_irx; + +static void ethStatusCheckCb(s32 alarm_id, u16 time, void *common) { + int threadID = *(int*)common; + iWakeupThread(threadID); +} + +static int WaitValidNetState(int (*checkingFunction)(void)) { + // Wait for a valid network status + int threadID = GetThreadId(); + + for (int retries = 0; checkingFunction() == 0; retries++) + { + // Sleep for 500ms + SetAlarm(500 * 16, ðStatusCheckCb, &threadID); + SleepThread(); + + if (retries >= 10) // 5s = 10 * 500ms + return -1; + } + return 0; +} + +static int ethGetNetIFLinkStatus(void) { + return NetManIoctl(NETMAN_NETIF_IOCTL_GET_LINK_STATUS, NULL, 0, NULL, 0) == NETMAN_NETIF_ETH_LINK_STATE_UP; +} + +static int ethWaitValidNetIFLinkState(void) { + return WaitValidNetState(ðGetNetIFLinkStatus); +} + +static int ethGetDHCPStatus(void) { + t_ip_info ip_info; + int result; + if ((result = ps2ip_getconfig("sm0", &ip_info)) < 0) return result; + + if (ip_info.dhcp_enabled) { + return ip_info.dhcp_status == DHCP_STATE_BOUND || ip_info.dhcp_status == DHCP_STATE_OFF; + } + return -1; +} + +static int ethWaitValidDHCPState(void) { + return WaitValidNetState(ðGetDHCPStatus); +} + +static int ethEnableDHCP(void) { + t_ip_info ip_info; + int result; + // SMAP is registered as the "sm0" device to the TCP/IP stack. + if ((result = ps2ip_getconfig("sm0", &ip_info)) < 0) return result; + + if (!ip_info.dhcp_enabled) { + ip_info.dhcp_enabled = 1; + return ps2ip_setconfig(&ip_info); + } + return 0; +} + +static void Networking_Setup(void) { + struct ip4_addr IP = { 0 }, NM = { 0 }, GW = { 0 }; + ps2ipInit(&IP, &NM, &GW); + ethEnableDHCP(); + + Platform_LogConst("Waiting for net link connection..."); + if(ethWaitValidNetIFLinkState() != 0) { + Platform_LogConst("Failed to establish net link"); + return; + } + + Platform_LogConst("Waiting for DHCP lease..."); + if (ethWaitValidDHCPState() != 0) { + Platform_LogConst("Failed to acquire DHCP lease"); + return; + } + Platform_LogConst("Network setup done"); +} + +static void Networking_Init(void) { + NetManInit(); +} + +static void Networking_LoadIOPModules(void) { + int ret; + + ret = SifExecModuleBuffer(DEV9_irx, size_DEV9_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer DEV9_irx failed: %i", &ret); + + ret = SifExecModuleBuffer(NETMAN_irx, size_NETMAN_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer NETMAN_irx failed: %i", &ret); + + ret = SifExecModuleBuffer(SMAP_irx, size_SMAP_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer SMAP_irx failed: %i", &ret); +} + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result ParseHost(const char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int res, i = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + res = getaddrinfo(host, portRaw, &hints, &result); + if (res == -NO_DATA) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next, i++) + { + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); + } + + freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + char str[NATIVE_STR_LEN]; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + if (inet_aton(str, &addr4->sin_addr) > 0) { + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(*addr4); + *numValidAddrs = 1; + return 0; + } + + return ParseHost(str, port, addrs, numValidAddrs); +} + +static cc_result GetSocketError(cc_socket s) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = 0; + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + + *s = socket(raw->sa_family, SOCK_STREAM, 0); + if (*s < 0) return *s; + + if (nonblocking) { + int blocking_raw = -1; // non-blocking mode + //ioctlsocket(*s, FIONBIO, &blocking_raw); TODO doesn't work + } + + res = connect(*s, raw, addr->size); + return res == -1 ? GetSocketError(*s) : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + Platform_Log1("PREPARE TO READ: %i", &count); + int recvCount = recv(s, data, count, 0); + Platform_Log1(" .. read %i", &recvCount); + if (recvCount != -1) { *modified = recvCount; return 0; } + + int ERR = GetSocketError(s); + Platform_Log1("ERR: %i", &ERR); + *modified = 0; return ERR; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + Platform_Log1("PREPARE TO WRITE: %i", &count); + int sentCount = send(s, data, count, 0); + Platform_Log1(" .. wrote %i", &sentCount); + if (sentCount != -1) { *modified = sentCount; return 0; } + + int ERR = GetSocketError(s); + Platform_Log1("ERR: %i", &ERR); + *modified = 0; return ERR; +} + +void Socket_Close(cc_socket s) { + shutdown(s, SHUT_RDWR); + close(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + fd_set read_set, write_set, error_set; + struct timeval time = { 0 }; + int selectCount; + + FD_ZERO(&read_set); + FD_SET(s, &read_set); + FD_ZERO(&write_set); + FD_SET(s, &write_set); + FD_ZERO(&error_set); + FD_SET(s, &error_set); + + selectCount = select(s + 1, &read_set, &write_set, &error_set, &time); + + Platform_Log4("SELECT %i = %h / %h / %h", &selectCount, &read_set, &write_set, &error_set); + if (selectCount == -1) { *success = false; return errno; } + *success = FD_ISSET(s, &write_set) != 0; return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + Platform_LogConst("POLL READ"); + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +static int tries; +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + Platform_Log1("POLL WRITE: %i", &res); + if (res || *writable) return res; + + // INPROGRESS error code returned if connect is still in progress + res = GetSocketError(s); + Platform_Log1("POLL FAIL: %i", &res); + if (res == EINPROGRESS) res = 0; + + if (tries++ > 20) { *writable = true; } + return res; +} + + +/*########################################################################################################################* +*----------------------------------------------------USB mass storage-----------------------------------------------------* +*#########################################################################################################################*/ +extern unsigned char USBD_irx[]; +extern unsigned int size_USBD_irx; + +extern unsigned char BDM_irx[]; +extern unsigned int size_BDM_irx; + +extern unsigned char BDMFS_FATFS_irx[]; +extern unsigned int size_BDMFS_FATFS_irx; + +extern unsigned char USBMASS_BD_irx[]; +extern unsigned int size_USBMASS_BD_irx; + +extern unsigned char USBHDFSD_irx[]; +extern unsigned int size_USBHDFSD_irx; + +extern unsigned char USBMASS_BD_irx[]; +extern unsigned int size_USBMASS_BD_irx; + +extern unsigned char USBMOUSE_irx[]; +extern unsigned int size_USBMOUSE_irx; + +static void USBStorage_LoadIOPModules(void) { + int ret; + // TODO: Seems that + // BDM, BDMFS_FATFS, USBMASS_BD - newer ? + // USBHDFSD - older ? + + ret = SifExecModuleBuffer(USBD_irx, size_USBD_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer USBD_irx failed: %i", &ret); + + //ret = SifExecModuleBuffer(USBHDFSD_irx, size_USBHDFSD_irx, 0, NULL, NULL); + //if (ret < 0) Platform_Log1("SifExecModuleBuffer USBHDFSD_irx failed: %i", &ret); + + ret = SifExecModuleBuffer(BDM_irx, size_BDM_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer BDM_irx failed: %i", &ret); + + ret = SifExecModuleBuffer(BDMFS_FATFS_irx, size_BDMFS_FATFS_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer BDMFS_FATFS_irx failed: %i", &ret); + + ret = SifExecModuleBuffer(USBMASS_BD_irx, size_USBMASS_BD_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer USBMASS_BD_irx failed: %i", &ret); + + ret = SifExecModuleBuffer(USBMOUSE_irx, size_USBMOUSE_irx, 0, NULL, NULL); + if (ret < 0) Platform_Log1("SifExecModuleBuffer USBMOUSE_irx failed: %i", &ret); +} + +// TODO Maybe needed ??? +/* +static void USBStorage_WaitUntilDeviceReady() { + io_stat_t sb; + Thread_Sleep(50); + + for (int retry = 0; retry < 50; retry++) + { + if (fioGetstat("mass:/", &sb) >= 0) return; + + nopdelay(); + } + Platform_LogConst("USB device still not ready ??"); +}*/ + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +// Note that resetting IOP does mean can't debug through ps2client +// https://github.com/libsdl-org/SDL/commit/d355ea9981358a1df335b1f7485ce94768bbf255 +// https://github.com/libsdl-org/SDL/pull/6022 +static void ResetIOP(void) { // reboots the IOP + SifInitRpc(0); + while (!SifIopReset("", 0)) { } + while (!SifIopSync()) { } +} + +static void LoadIOPModules(void) { + int ret; + + // file I/O module + ret = SifLoadModule("rom0:FILEIO", 0, NULL); + if (ret < 0) Platform_Log1("SifLoadModule FILEIO failed: %i", &ret); + sbv_patch_fileio(); + + // serial I/O module (needed for memory card & input pad modules) + ret = SifLoadModule("rom0:SIO2MAN", 0, NULL); + if (ret < 0) Platform_Log1("SifLoadModule SIO2MAN failed: %i", &ret); + + + // memory card module + ret = SifLoadModule("rom0:MCMAN", 0, NULL); + if (ret < 0) Platform_Log1("SifLoadModule MCMAN failed: %i", &ret); + + // memory card server module + ret = SifLoadModule("rom0:MCSERV", 0, NULL); + if (ret < 0) Platform_Log1("SifLoadModule MCSERV failed: %i", &ret); + + + // Input pad module + ret = SifLoadModule("rom0:PADMAN", 0, NULL); + if (ret < 0) Platform_Log1("SifLoadModule PADMAN failed: %i", &ret); + +} + +void Platform_Init(void) { + //InitDebug(); + ResetIOP(); + SifInitRpc(0); + SifLoadFileInit(); + SifInitIopHeap(); + sbv_patch_enable_lmb(); // Allows loading IRX modules from a buffer in EE RAM + + LoadIOPModules(); + USBStorage_LoadIOPModules(); + //USBStorage_WaitUntilDeviceReady(); + + Networking_LoadIOPModules(); + Networking_Init(); + Networking_Setup(); + + // Create root directory + int res = fioMkdir("mass:/ClassiCube"); + Platform_Log1("ROOT CREATE %i", &res); +} + +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_PS3.c b/src/Platform_PS3.c new file mode 100644 index 0000000..3eb39ca --- /dev/null +++ b/src/Platform_PS3.c @@ -0,0 +1,490 @@ +#include "Core.h" +#if defined PLAT_PS3 + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = 0x80010006; // ENOENT; +const cc_result ReturnCode_SocketInProgess = NET_EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = NET_EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = 0x80010014; // EEXIST + +const char* Platform_AppNameSuffix = " PS3"; + +SYS_PROCESS_PARAM(1001, 256 * 1024); // 256kb stack size + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Log(const char* msg, int len) { + u32 done = 0; + sysTtyWrite(STDOUT_FILENO, msg, len, &done); + sysTtyWrite(STDOUT_FILENO, "\n", 1, &done); +} + +TimeMS DateTime_CurrentUTC(void) { + u64 sec, nanosec; + sysGetCurrentTime(&sec, &nanosec); + return sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct timeval cur; + struct tm loc_time; + gettimeofday(&cur, NULL); + localtime_r(&cur.tv_sec, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +#define NS_PER_SEC 1000000000ULL + +cc_uint64 Stopwatch_Measure(void) { + u64 sec, nsec; + sysGetCurrentTime(&sec, &nsec); + return sec * NS_PER_SEC + nsec; +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return (end - beg) / 1000; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("/dev_hdd0/ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + /* read/write/search permissions for owner and group, and with read/search permissions for others. */ + /* TODO: Is the default mode in all cases */ + return sysLv2FsMkdir(str, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + sysFSStat sb; + GetNativePath(str, path); + return sysLv2FsStat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + sysFSDirent entry; + char* src; + int dir_fd, res; + + GetNativePath(str, dirPath); + if ((res = sysLv2FsOpenDir(str, &dir_fd))) return res; + + for (;;) + { + u64 read = 0; + if ((res = sysLv2FsReadDir(dir_fd, &entry, &read))) return res; + if (!read) break; // end of entries + + // ignore . and .. entry + src = entry.d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + String_InitArray(path, pathBuffer); + String_Format1(&path, "%s/", dirPath); + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + + if (entry.d_type == DT_DIR) { + res = Directory_Enum(&path, obj, callback); + if (res) break; + } else { + callback(&path, obj); + } + } + + sysLv2FsCloseDir(dir_fd); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + int fd = -1; + + int access = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + int res = sysLv2FsOpen(str, mode, &fd, access, NULL, 0); + + if (res) { + *file = -1; return res; + } else { + // TODO: is this actually needed? + if (mode & SYS_O_CREAT) sysLv2FsChmod(str, access); + *file = fd; return 0; + } +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, SYS_O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, SYS_O_RDWR | SYS_O_CREAT | SYS_O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, SYS_O_RDWR | SYS_O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + u64 read = 0; + int res = sysLv2FsRead(file, data, count, &read); + + *bytesRead = read; + return res; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + u64 wrote = 0; + int res = sysLv2FsWrite(file, data, count, &wrote); + + *bytesWrote = wrote; + return res; +} + +cc_result File_Close(cc_file file) { + return sysLv2FsClose(file); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[] = { SEEK_SET, SEEK_CUR, SEEK_END }; + u64 position = 0; + return sysLv2FsLSeek64(file, offset, modes[seekType], &position); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + u64 position = 0; + int res = sysLv2FsLSeek64(file, 0, SEEK_CUR, &position); + + *pos = position; + return res; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + sysFSStat st; + int res = sysLv2FsFStat(file, &st); + + *len = st.st_size; + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + sysUsleep(milliseconds * 1000); +} + +static void ExecThread(void* param) { + ((Thread_StartFunc)param)(); +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + sys_ppu_thread_t* thread = (sys_ppu_thread_t*)Mem_Alloc(1, sizeof(sys_ppu_thread_t), "thread"); + *handle = thread; + + int res = sysThreadCreate(thread, ExecThread, (void*)func, + 0, stackSize, THREAD_JOINABLE, name); + if (res) Logger_Abort2(res, "Creating thread"); +} + +void Thread_Detach(void* handle) { + sys_ppu_thread_t* thread = (sys_ppu_thread_t*)handle; + int res = sysThreadDetach(*thread); + if (res) Logger_Abort2(res, "Detaching thread"); + Mem_Free(thread); +} + +void Thread_Join(void* handle) { + u64 retVal; + sys_ppu_thread_t* thread = (sys_ppu_thread_t*)handle; + int res = sysThreadJoin(*thread, &retVal); + if (res) Logger_Abort2(res, "Joining thread"); + Mem_Free(thread); +} + +void* Mutex_Create(const char* name) { + sys_mutex_attr_t attr; + sysMutexAttrInitialize(attr); + + sys_mutex_t* mutex = (sys_mutex_t*)Mem_Alloc(1, sizeof(sys_mutex_t), "mutex"); + int res = sysMutexCreate(mutex, &attr); + if (res) Logger_Abort2(res, "Creating mutex"); + return mutex; +} + +void Mutex_Free(void* handle) { + sys_mutex_t* mutex = (sys_mutex_t*)handle; + int res = sysMutexDestroy(*mutex); + if (res) Logger_Abort2(res, "Destroying mutex"); + Mem_Free(mutex); +} + +void Mutex_Lock(void* handle) { + sys_mutex_t* mutex = (sys_mutex_t*)handle; + int res = sysMutexLock(*mutex, 0); + if (res) Logger_Abort2(res, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + sys_mutex_t* mutex = (sys_mutex_t*)handle; + int res = sysMutexUnlock(*mutex); + if (res) Logger_Abort2(res, "Unlocking mutex"); +} + +void* Waitable_Create(const char* name) { + sys_sem_attr_t attr = { 0 }; + attr.attr_protocol = SYS_SEM_ATTR_PROTOCOL; + attr.attr_pshared = SYS_SEM_ATTR_PSHARED; + + sys_sem_t* sem = (sys_sem_t*)Mem_Alloc(1, sizeof(sys_sem_t), "waitable"); + int res = sysSemCreate(sem, &attr, 0, 1000000); + if (res) Logger_Abort2(res, "Creating waitable"); + + return sem; +} + +void Waitable_Free(void* handle) { + sys_sem_t* sem = (sys_sem_t*)handle; + + int res = sysSemDestroy(*sem); + if (res) Logger_Abort2(res, "Destroying waitable"); + Mem_Free(sem); +} + +void Waitable_Signal(void* handle) { + sys_sem_t* sem = (sys_sem_t*)handle; + int res = sysSemPost(*sem, 1); + if (res) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + sys_sem_t* sem = (sys_sem_t*)handle; + int res = sysSemWait(*sem, 0); + if (res) Logger_Abort2(res, "Waitable wait"); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + sys_sem_t* sem = (sys_sem_t*)handle; + int res = sysSemWait(*sem, milliseconds * 1000); + if (res) Logger_Abort2(res, "Waitable wait for"); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +union SocketAddress { + struct sockaddr raw; + struct sockaddr_in v4; +}; +static cc_result ParseHost(char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct net_hostent* res = netGetHostByName(host); + struct sockaddr_in* addr4; + if (!res) return net_h_errno; + + // Must have at least one IPv4 address + if (res->h_addrtype != AF_INET) return ERR_INVALID_ARGUMENT; + if (!res->h_addr_list) return ERR_INVALID_ARGUMENT; + + // each address pointer is only 4 bytes long + u32* addr_list = (u32*)res->h_addr_list; + char* src_addr; + int i; + + for (i = 0; i < SOCKET_MAX_ADDRS; i++) + { + src_addr = (char*)addr_list[i]; + if (!src_addr) break; + addrs[i].size = sizeof(struct sockaddr_in); + addr4 = (struct sockaddr_in*)addrs[i].data; + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + addr4->sin_addr = *(struct in_addr*)src_addr; + } + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + if (inet_aton(str, &addr4->sin_addr) > 0) { + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(*addr4); + *numValidAddrs = 1; + return 0; + } + + return ParseHost(str, port, addrs, numValidAddrs); +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + + res = netSocket(raw->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (res < 0) return net_errno; + *s = res; + + if (nonblocking) { + int on = 1; + netSetSockOpt(*s, SOL_SOCKET, SO_NBIO, &on, sizeof(int)); + } + + res = netConnect(*s, raw, addr->size); + return res < 0 ? net_errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int res = netRecv(s, data, count, 0); + if (res < 0) return net_errno; + + *modified = res; return 0; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int res = netSend(s, data, count, 0); + if (res < 0) return net_errno; + + *modified = res; return 0; +} + +void Socket_Close(cc_socket s) { + netShutdown(s, SHUT_RDWR); + netClose(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + int flags; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + + if (netPoll(&pfd, 1, 0) < 0) return net_errno; + + /* to match select, closed socket still counts as readable */ + flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + // https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation + netGetSockOpt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Init(void) { + netInitialize(); + // Create root directory + Directory_Create(&String_Empty); +} + +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_PSP.c b/src/Platform_PSP.c new file mode 100644 index 0000000..984fa4a --- /dev/null +++ b/src/Platform_PSP.c @@ -0,0 +1,476 @@ +#include "Core.h" +#if defined CC_BUILD_PSP +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " PSP"; + +PSP_MODULE_INFO("ClassiCube", PSP_MODULE_USER, 1, 0); +PSP_MAIN_THREAD_ATTR(PSP_THREAD_ATTR_USER); + +PSP_DISABLE_AUTOSTART_PTHREAD() // reduces .elf size by 140 kb + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return end - beg; +} + +void Platform_Log(const char* msg, int len) { + int fd = sceKernelStdout(); + sceIoWrite(fd, msg, len); + + //sceIoDevctl("emulator:", 2, msg, len, NULL, 0); + //cc_string str = String_Init(msg, len, len); + //cc_file file = 0; + //File_Open(&file, &str); + //File_Close(file); +} + +TimeMS DateTime_CurrentUTC(void) { + struct SceKernelTimeval cur; + sceKernelLibcGettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + ScePspDateTime curTime; + sceRtcGetCurrentClockLocalTime(&curTime); + + t->year = curTime.year; + t->month = curTime.month; + t->day = curTime.day; + t->hour = curTime.hour; + t->minute = curTime.minute; + t->second = curTime.second; +} + +#define US_PER_SEC 1000000ULL +cc_uint64 Stopwatch_Measure(void) { + // TODO: sceKernelGetSystemTimeWide + struct SceKernelTimeval cur; + sceKernelLibcGettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec * US_PER_SEC + cur.tv_usec; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("ms0:/PSP/GAME/ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +#define GetSCEResult(result) (result >= 0 ? 0 : result & 0xFFFF) + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + int result = sceIoMkdir(str, 0777); + return GetSCEResult(result); +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + SceIoStat sb; + GetNativePath(str, path); + return sceIoGetstat(str, &sb) == 0 && (sb.st_attr & FIO_SO_IFREG) != 0; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + int res; + + GetNativePath(str, dirPath); + SceUID uid = sceIoDopen(str); + if (uid < 0) return GetSCEResult(uid); // error + + String_InitArray(path, pathBuffer); + SceIoDirent entry; + + while ((res = sceIoDread(uid, &entry)) > 0) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + // ignore . and .. entry (PSP does return them) + char* src = entry.d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + + if (entry.d_stat.st_attr & FIO_SO_IFDIR) { + res = Directory_Enum(&path, obj, callback); + if (res) break; + } else { + callback(&path, obj); + } + } + + sceIoDclose(uid); + return GetSCEResult(res); +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + int result = sceIoOpen(str, mode, 0777); + *file = result; + return GetSCEResult(result); +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, PSP_O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, PSP_O_RDWR | PSP_O_CREAT | PSP_O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, PSP_O_RDWR | PSP_O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + int result = sceIoRead(file, data, count); + *bytesRead = result; + return GetSCEResult(result); +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + int result = sceIoWrite(file, data, count); + *bytesWrote = result; + return GetSCEResult(result); +} + +cc_result File_Close(cc_file file) { + int result = sceIoClose(file); + return GetSCEResult(result); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { PSP_SEEK_SET, PSP_SEEK_CUR, PSP_SEEK_END }; + + int result = sceIoLseek32(file, offset, modes[seekType]); + return GetSCEResult(result); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + int result = sceIoLseek32(file, 0, PSP_SEEK_CUR); + *pos = result; + return GetSCEResult(result); +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + int curPos = sceIoLseek32(file, 0, PSP_SEEK_CUR); + if (curPos < 0) { *len = -1; return GetSCEResult(curPos); } + + *len = sceIoLseek32(file, 0, PSP_SEEK_END); + sceIoLseek32(file, curPos, PSP_SEEK_SET); // restore position + return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +// !!! NOTE: PSP uses cooperative multithreading (not preemptive multithreading) !!! +void Thread_Sleep(cc_uint32 milliseconds) { + sceKernelDelayThread(milliseconds * 1000); +} + +static int ExecThread(unsigned int argc, void *argv) { + Thread_StartFunc* func = (Thread_StartFunc*)argv; + (*func)(); + return 0; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + #define CC_THREAD_PRIORITY 17 // TODO: 18? + #define CC_THREAD_ATTRS 0 // TODO PSP_THREAD_ATTR_VFPU? + Thread_StartFunc func_ = func; + + int threadID = sceKernelCreateThread(name, ExecThread, CC_THREAD_PRIORITY, + stackSize, CC_THREAD_ATTRS, NULL); + + *handle = (void*)threadID; + sceKernelStartThread(threadID, sizeof(func_), (void*)&func_); +} + +void Thread_Detach(void* handle) { + sceKernelDeleteThread((int)handle); // TODO don't call this?? +} + +void Thread_Join(void* handle) { + sceKernelWaitThreadEnd((int)handle, NULL); + sceKernelDeleteThread((int)handle); +} + +void* Mutex_Create(const char* name) { + SceLwMutexWorkarea* ptr = (SceLwMutexWorkarea*)Mem_Alloc(1, sizeof(SceLwMutexWorkarea), "mutex"); + int res = sceKernelCreateLwMutex(ptr, name, 0, 0, NULL); + if (res) Logger_Abort2(res, "Creating mutex"); + return ptr; +} + +void Mutex_Free(void* handle) { + int res = sceKernelDeleteLwMutex((SceLwMutexWorkarea*)handle); + if (res) Logger_Abort2(res, "Destroying mutex"); + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + int res = sceKernelLockLwMutex((SceLwMutexWorkarea*)handle, 1, NULL); + if (res) Logger_Abort2(res, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + int res = sceKernelUnlockLwMutex((SceLwMutexWorkarea*)handle, 1); + if (res) Logger_Abort2(res, "Unlocking mutex"); +} + +void* Waitable_Create(const char* name) { + int evid = sceKernelCreateEventFlag(name, PSP_EVENT_WAITMULTIPLE, 0, NULL); + if (evid < 0) Logger_Abort2(evid, "Creating waitable"); + return (void*)evid; +} + +void Waitable_Free(void* handle) { + sceKernelDeleteEventFlag((int)handle); +} + +void Waitable_Signal(void* handle) { + int res = sceKernelSetEventFlag((int)handle, 0x1); + if (res < 0) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + u32 match; + int res = sceKernelWaitEventFlag((int)handle, 0x1, PSP_EVENT_WAITAND | PSP_EVENT_WAITCLEAR, &match, NULL); + if (res < 0) Logger_Abort2(res, "Event wait"); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + SceUInt timeout = milliseconds * 1000; + u32 match; + int res = sceKernelWaitEventFlag((int)handle, 0x1, PSP_EVENT_WAITAND | PSP_EVENT_WAITCLEAR, &match, &timeout); + if (res < 0) Logger_Abort2(res, "Event timed wait"); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct sockaddr_in* addr4 = (struct sockaddr_in*)addrs[0].data; + char str[NATIVE_STR_LEN]; + char buf[1024]; + int rid, ret; + + String_EncodeUtf8(str, address); + *numValidAddrs = 1; + + if (sceNetInetInetPton(AF_INET, str, &addr4->sin_addr) <= 0) { + /* Fallback to resolving as DNS name */ + if (sceNetResolverCreate(&rid, buf, sizeof(buf)) < 0) + return ERR_INVALID_ARGUMENT; + + ret = sceNetResolverStartNtoA(rid, str, &addr4->sin_addr, 1 /* timeout */, 5 /* retries */); + sceNetResolverDelete(rid); + if (ret < 0) return ret; + } + + addr4->sin_family = AF_INET; + addr4->sin_port = htons(port); + + addrs[0].size = sizeof(*addr4); + return 0; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + + *s = sceNetInetSocket(raw->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (*s < 0) return sceNetInetGetErrno(); + + if (nonblocking) { + int on = 1; + sceNetInetSetsockopt(*s, SOL_SOCKET, SO_NONBLOCK, &on, sizeof(int)); + } + + res = sceNetInetConnect(*s, raw, addr->size); + return res < 0 ? sceNetInetGetErrno() : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = sceNetInetRecv(s, data, count, 0); + if (recvCount >= 0) { *modified = recvCount; return 0; } + + *modified = 0; + return sceNetInetGetErrno(); +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = sceNetInetSend(s, data, count, 0); + if (sentCount >= 0) { *modified = sentCount; return 0; } + + *modified = 0; + return sceNetInetGetErrno(); +} + +void Socket_Close(cc_socket s) { + sceNetInetShutdown(s, SHUT_RDWR); + sceNetInetClose(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + fd_set set; + struct SceNetInetTimeval time = { 0 }; + int selectCount; + + FD_ZERO(&set); + FD_SET(s, &set); + + if (mode == SOCKET_POLL_READ) { + selectCount = sceNetInetSelect(s + 1, &set, NULL, NULL, &time); + } else { + selectCount = sceNetInetSelect(s + 1, NULL, &set, NULL, &time); + } + + if (selectCount == -1) { + *success = false; + return errno; + } + + *success = FD_ISSET(s, &set) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + // https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation + sceNetInetGetsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static void InitNetworking(void) { + sceUtilityLoadNetModule(PSP_NET_MODULE_COMMON); + sceUtilityLoadNetModule(PSP_NET_MODULE_INET); + int res; + + res = sceNetInit(128 * 1024, 0x20, 4096, 0x20, 4096); + if (res < 0) { Platform_Log1("sceNetInit failed: %i", &res); return; } + + res = sceNetInetInit(); + if (res < 0) { Platform_Log1("sceNetInetInit failed: %i", &res); return; } + + res = sceNetResolverInit(); + if (res < 0) { Platform_Log1("sceNetResolverInit failed: %i", &res); return; } + + res = sceNetApctlInit(10 * 1024, 0x30); + if (res < 0) { Platform_Log1("sceNetApctlInit failed: %i", &res); return; } + + res = sceNetApctlConnect(1); // 1 = first profile + if (res) { Platform_Log1("sceNetApctlConnect failed: %i", &res); return; } + + for (int try = 0; try < 20; try++) { + int state; + res = sceNetApctlGetState(&state); + if (res) { Platform_Log1("sceNetApctlGetState failed: %i", &res); return; } + + if (state == PSP_NET_APCTL_STATE_GOT_IP) break; + + // not successful yet? try polling again in 50 ms + sceKernelDelayThread(50 * 1000); + } +} + +void Platform_Init(void) { + InitNetworking(); + /*pspDebugSioInit();*/ + + // Disabling FPU exceptions avoids sometimes crashing with this line in Physics.c + // *tx = vel->x == 0.0f ? MATH_LARGENUM : Math_AbsF(dx / vel->x); + // TODO: work out why this error is actually happening (inexact or underflow?) and properly fix it + pspSdkDisableFPUExceptions(); + + // Create root directory + Directory_Create(&String_Empty); +} +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_PSVita.c b/src/Platform_PSVita.c new file mode 100644 index 0000000..7c57537 --- /dev/null +++ b/src/Platform_PSVita.c @@ -0,0 +1,432 @@ +#include "Core.h" +#if defined CC_BUILD_PSVITA +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" + +#include +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = SCE_NET_ERROR_EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = SCE_NET_ERROR_EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " PS Vita"; +static int epoll_id; +static int stdout_fd; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return end - beg; +} + +void Platform_Log(const char* msg, int len) { + if (!stdout_fd) stdout_fd = sceKernelGetStdout(); + sceIoWrite(stdout_fd, msg, len); +} + +TimeMS DateTime_CurrentUTC(void) { + struct SceKernelTimeval cur; + sceKernelLibcGettimeofday(&cur, NULL); + return (cc_uint64)cur.sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + SceDateTime curTime; + sceRtcGetCurrentClockLocalTime(&curTime); + + t->year = curTime.year; + t->month = curTime.month; + t->day = curTime.day; + t->hour = curTime.hour; + t->minute = curTime.minute; + t->second = curTime.second; +} + +#define US_PER_SEC 1000000ULL +cc_uint64 Stopwatch_Measure(void) { + // TODO: sceKernelGetSystemTimeWide + struct SceKernelTimeval cur; + sceKernelLibcGettimeofday(&cur, NULL); + return (cc_uint64)cur.sec * US_PER_SEC + cur.usec; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("ux0:data/ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +#define GetSCEResult(result) (result >= 0 ? 0 : result & 0xFFFF) + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + int result = sceIoMkdir(str, 0777); + return GetSCEResult(result); +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + SceIoStat sb; + GetNativePath(str, path); + return sceIoGetstat(str, &sb) == 0 && SCE_S_ISREG(sb.st_mode) != 0; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + int res; + + GetNativePath(str, dirPath); + SceUID uid = sceIoDopen(str); + if (uid < 0) return GetSCEResult(uid); // error + + String_InitArray(path, pathBuffer); + SceIoDirent entry; + + while ((res = sceIoDread(uid, &entry)) > 0) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + // ignore . and .. entry (PSP does return them) + char* src = entry.d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + + if (entry.d_stat.st_attr & SCE_SO_IFDIR) { + res = Directory_Enum(&path, obj, callback); + if (res) break; + } else { + callback(&path, obj); + } + } + + sceIoDclose(uid); + return GetSCEResult(res); +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + + int result = sceIoOpen(str, mode, 0777); + *file = result; + return GetSCEResult(result); +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, SCE_O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, SCE_O_RDWR | SCE_O_CREAT | SCE_O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, SCE_O_RDWR | SCE_O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + int result = sceIoRead(file, data, count); + *bytesRead = result; + return GetSCEResult(result); +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + int result = sceIoWrite(file, data, count); + *bytesWrote = result; + return GetSCEResult(result); +} + +cc_result File_Close(cc_file file) { + int result = sceIoClose(file); + return GetSCEResult(result); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SCE_SEEK_SET, SCE_SEEK_CUR, SCE_SEEK_END }; + + int result = sceIoLseek32(file, offset, modes[seekType]); + return GetSCEResult(result); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + int result = sceIoLseek32(file, 0, SCE_SEEK_CUR); + *pos = result; + return GetSCEResult(result); +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + int curPos = sceIoLseek32(file, 0, SCE_SEEK_CUR); + if (curPos < 0) { *len = -1; return GetSCEResult(curPos); } + + *len = sceIoLseek32(file, 0, SCE_SEEK_END); + sceIoLseek32(file, curPos, SCE_SEEK_SET); // restore position + return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +// !!! NOTE: PSP uses cooperative multithreading (not preemptive multithreading) !!! +void Thread_Sleep(cc_uint32 milliseconds) { + sceKernelDelayThread(milliseconds * 1000); +} + +static int ExecThread(unsigned int argc, void *argv) { + Thread_StartFunc* func = (Thread_StartFunc*)argv; + (*func)(); + return 0; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + #define CC_THREAD_PRIORITY 0x10000100 + #define CC_THREAD_ATTRS 0 // TODO PSP_THREAD_ATTR_VFPU? + Thread_StartFunc func_ = func; + + int threadID = sceKernelCreateThread(name, ExecThread, CC_THREAD_PRIORITY, + stackSize, CC_THREAD_ATTRS, 0, NULL); + + *handle = (int)threadID; + sceKernelStartThread(threadID, sizeof(func_), (void*)&func_); +} + +void Thread_Detach(void* handle) { + sceKernelDeleteThread((int)handle); // TODO don't call this?? +} + +void Thread_Join(void* handle) { + sceKernelWaitThreadEnd((int)handle, NULL, NULL); + sceKernelDeleteThread((int)handle); +} + +void* Mutex_Create(const char* name) { + SceKernelLwMutexWork* ptr = (SceKernelLwMutexWork*)Mem_Alloc(1, sizeof(SceKernelLwMutexWork), "mutex"); + int res = sceKernelCreateLwMutex(ptr, name, 0, 0, NULL); + if (res) Logger_Abort2(res, "Creating mutex"); + return ptr; +} + +void Mutex_Free(void* handle) { + int res = sceKernelDeleteLwMutex((SceKernelLwMutexWork*)handle); + if (res) Logger_Abort2(res, "Destroying mutex"); + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + int res = sceKernelLockLwMutex((SceKernelLwMutexWork*)handle, 1, NULL); + if (res) Logger_Abort2(res, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + int res = sceKernelUnlockLwMutex((SceKernelLwMutexWork*)handle, 1); + if (res) Logger_Abort2(res, "Unlocking mutex"); +} + +void* Waitable_Create(const char* name) { + int evid = sceKernelCreateEventFlag(name, SCE_EVENT_WAITMULTIPLE, 0, NULL); + if (evid < 0) Logger_Abort2(evid, "Creating waitable"); + return (void*)evid; +} + +void Waitable_Free(void* handle) { + sceKernelDeleteEventFlag((int)handle); +} + +void Waitable_Signal(void* handle) { + int res = sceKernelSetEventFlag((int)handle, 0x1); + if (res < 0) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + unsigned int match; + int res = sceKernelWaitEventFlag((int)handle, 0x1, SCE_EVENT_WAITAND | SCE_EVENT_WAITCLEAR, &match, NULL); + if (res < 0) Logger_Abort2(res, "Event wait"); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + SceUInt timeout = milliseconds * 1000; + unsigned int match; + int res = sceKernelWaitEventFlag((int)handle, 0x1, SCE_EVENT_WAITAND | SCE_EVENT_WAITCLEAR, &match, &timeout); + if (res < 0) Logger_Abort2(res, "Event timed wait"); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct SceNetSockaddrIn* addr4 = (struct SceNetSockaddrIn*)addrs[0].data; + char str[NATIVE_STR_LEN]; + char buf[1024]; + int rid, ret; + + String_EncodeUtf8(str, address); + *numValidAddrs = 1; + + if (sceNetInetPton(SCE_NET_AF_INET, str, &addr4->sin_addr) <= 0) { + /* Fallback to resolving as DNS name */ + rid = sceNetResolverCreate("CC resolver", NULL, 0); + if (rid < 0) return ERR_INVALID_ARGUMENT; + + ret = sceNetResolverStartNtoa(rid, str, &addr4->sin_addr, 0, 0, 0); + sceNetResolverDestroy(rid); + if (ret) return ret; + } + + addr4->sin_family = SCE_NET_AF_INET; + addr4->sin_port = sceNetHtons(port); + + addrs[0].size = sizeof(*addr4); + return 0; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct SceNetSockaddr* raw = (struct SceNetSockaddr*)addr->data; + int res; + + *s = sceNetSocket("CC socket", raw->sa_family, SCE_NET_SOCK_STREAM, SCE_NET_IPPROTO_TCP); + if (*s < 0) return *s; + + if (nonblocking) { + int on = 1; + sceNetSetsockopt(*s, SCE_NET_SOL_SOCKET, SCE_NET_SO_NBIO, &on, sizeof(int)); + } + + res = sceNetConnect(*s, raw, addr->size); + return res; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = sceNetRecv(s, data, count, 0); + if (recvCount >= 0) { *modified = recvCount; return 0; } + + *modified = 0; + return recvCount; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = sceNetSend(s, data, count, 0); + if (sentCount >= 0) { *modified = sentCount; return 0; } + + *modified = 0; + return sentCount; +} + +void Socket_Close(cc_socket s) { + sceNetShutdown(s, SCE_NET_SHUT_RDWR); + sceNetSocketClose(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + SceNetEpollEvent ev = { 0 }; + // to match select, closed socket still counts as readable + int flags = mode == SOCKET_POLL_READ ? (SCE_NET_EPOLLIN | SCE_NET_EPOLLHUP) : SCE_NET_EPOLLOUT; + int res, num_events; + ev.data.fd = s; + ev.events = flags; + + if ((res = sceNetEpollControl(epoll_id, SCE_NET_EPOLL_CTL_ADD, s, &ev))) return res; + num_events = sceNetEpollWait(epoll_id, &ev, 1, 0); + sceNetEpollControl(epoll_id, SCE_NET_EPOLL_CTL_DEL, s, NULL); + + if (num_events < 0) return num_events; + if (num_events == 0) { *success = false; return 0; } + + *success = (ev.events & flags) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + uint32_t resultSize = sizeof(uint32_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + // https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation + sceNetGetsockopt(s, SCE_NET_SOL_SOCKET, SCE_NET_SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static char net_memory[512 * 1024] __attribute__ ((aligned (16))); // TODO is just 256 kb enough ? + +static void InitNetworking(void) { + sceSysmoduleLoadModule(SCE_SYSMODULE_NET); + SceNetInitParam param; + + param.memory = net_memory; + param.size = sizeof(net_memory); + param.flags = 0; + + int ret = sceNetInit(¶m); + if (ret < 0) Platform_Log1("Network init failed: %i", &ret); +} + +void Platform_Init(void) { + /*pspDebugSioInit();*/ + InitNetworking(); + epoll_id = sceNetEpollCreate("CC poll", 0); + // Create root directory + Directory_Create(&String_Empty); +} +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_Posix.c b/src/Platform_Posix.c new file mode 100644 index 0000000..2c487a8 --- /dev/null +++ b/src/Platform_Posix.c @@ -0,0 +1,1578 @@ +#include "Core.h" +#if defined CC_BUILD_POSIX + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "SystemFonts.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const cc_result ReturnCode_FileShareViolation = 1000000000; /* TODO: not used apparently */ +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; + +#if defined CC_BUILD_ANDROID +const char* Platform_AppNameSuffix = " android alpha"; +#elif defined CC_BUILD_IOS +const char* Platform_AppNameSuffix = " iOS alpha"; +#else +const char* Platform_AppNameSuffix = ""; +#endif +cc_bool Platform_SingleProcess; + +/* Operating system specific include files */ +#if defined CC_BUILD_DARWIN +#include +#include +#if defined CC_BUILD_MACOS +#include +#endif +#elif defined CC_BUILD_SOLARIS +#include +#include +#elif defined CC_BUILD_BSD +#include +#elif defined CC_BUILD_HAIKU || defined CC_BUILD_BEOS +/* TODO: Use load_image/resume_thread instead of fork */ +/* Otherwise opening browser never works because fork fails */ +#include +#elif defined CC_BUILD_OS2 +#include +#define INCL_DOS +#define INCL_DOSERRORS +#define INCL_PM +#include +#endif + + +/*########################################################################################################################* +*---------------------------------------------------------Memory----------------------------------------------------------* +*#########################################################################################################################*/ +void* Mem_Set(void* dst, cc_uint8 value, unsigned numBytes) { return memset( dst, value, numBytes); } +void* Mem_Copy(void* dst, const void* src, unsigned numBytes) { return memcpy( dst, src, numBytes); } +void* Mem_Move(void* dst, const void* src, unsigned numBytes) { return memmove(dst, src, numBytes); } + +void* Mem_TryAlloc(cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? malloc(size) : NULL; +} + +void* Mem_TryAllocCleared(cc_uint32 numElems, cc_uint32 elemsSize) { + return calloc(numElems, elemsSize); +} + +void* Mem_TryRealloc(void* mem, cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? realloc(mem, size) : NULL; +} + +void Mem_Free(void* mem) { + if (mem) free(mem); +} + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_ANDROID +/* implemented in Platform_Android.c */ +#elif defined CC_BUILD_IOS +/* implemented in interop_ios.m */ +#else +void Platform_Log(const char* msg, int len) { + write(STDOUT_FILENO, msg, len); + write(STDOUT_FILENO, "\n", 1); +} +#endif + +TimeMS DateTime_CurrentUTC(void) { + struct timeval cur; + gettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct timeval cur; + struct tm loc_time; + gettimeofday(&cur, NULL); + localtime_r(&cur.tv_sec, &loc_time); + + t->year = loc_time.tm_year + 1900; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +#define NS_PER_SEC 1000000000ULL + +#if defined CC_BUILD_HAIKU || defined CC_BUILD_BEOS +/* Implemented in interop_BeOS.cpp */ +#elif defined CC_BUILD_DARWIN +static cc_uint64 sw_freqMul, sw_freqDiv; +static void Stopwatch_Init(void) { + mach_timebase_info_data_t tb = { 0 }; + mach_timebase_info(&tb); + + sw_freqMul = tb.numer; + /* tb.denom may be large, so multiplying by 1000 overflows 32 bits */ + /* (one powerpc system had tb.denom of 33329426) */ + sw_freqDiv = (cc_uint64)tb.denom * 1000; +} + +cc_uint64 Stopwatch_Measure(void) { return mach_absolute_time(); } + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return ((end - beg) * sw_freqMul) / sw_freqDiv; +} +#elif defined CC_BUILD_SOLARIS +/* https://docs.oracle.com/cd/E86824_01/html/E54766/gethrtime-3c.html */ +/* The gethrtime() function returns the current high-resolution real time. Time is expressed as nanoseconds since some arbitrary time in the past */ +cc_uint64 Stopwatch_Measure(void) { return gethrtime(); } + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return (end - beg) / 1000; +} +#else +/* clock_gettime is optional, see http://pubs.opengroup.org/onlinepubs/009696899/functions/clock_getres.html */ +/* "... These functions are part of the Timers option and need not be available on all implementations..." */ +cc_uint64 Stopwatch_Measure(void) { + struct timespec t; + #ifdef CC_BUILD_IRIX + clock_gettime(CLOCK_REALTIME, &t); + #else + /* TODO: CLOCK_MONOTONIC_RAW ?? */ + clock_gettime(CLOCK_MONOTONIC, &t); + #endif + return (cc_uint64)t.tv_sec * NS_PER_SEC + t.tv_nsec; +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return (end - beg) / 1000; +} +#endif + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_ANDROID +/* implemented in Platform_Android.c */ +#elif defined CC_BUILD_IOS +/* implemented in interop_ios.m */ +#else +void Directory_GetCachePath(cc_string* path) { } +#endif + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, path); + /* read/write/search permissions for owner and group, and with read/search permissions for others. */ + /* TODO: Is the default mode in all cases */ + return mkdir(str, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == -1 ? errno : 0; +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + struct stat sb; + String_EncodeUtf8(str, path); + return stat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + DIR* dirPtr; + struct dirent* entry; + char* src; + int len, res, is_dir; + + String_EncodeUtf8(str, dirPath); + dirPtr = opendir(str); + if (!dirPtr) return errno; + + /* POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." */ + /* errno is sometimes leftover from previous calls, so always reset it before readdir gets called */ + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + /* ignore . and .. entry */ + src = entry->d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + len = String_Length(src); + String_AppendUtf8(&path, src, len); + +#if defined CC_BUILD_HAIKU || defined CC_BUILD_SOLARIS || defined CC_BUILD_IRIX || defined CC_BUILD_BEOS + { + char full_path[NATIVE_STR_LEN]; + struct stat sb; + String_EncodeUtf8(full_path, &path); + is_dir = stat(full_path, &sb) == 0 && S_ISDIR(sb.st_mode); + } +#else + is_dir = entry->d_type == DT_DIR; + /* TODO: fallback to stat when this fails */ +#endif + + if (is_dir) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; /* return code from readdir */ + closedir(dirPtr); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, path); + + *file = open(str, mode, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + return *file == -1 ? errno : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { +#if !defined CC_BUILD_OS2 + return File_Do(file, path, O_RDONLY); +#else + return File_Do(file, path, O_RDONLY | O_BINARY); +#endif +} +cc_result File_Create(cc_file* file, const cc_string* path) { +#if !defined CC_BUILD_OS2 + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +#else + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC | O_BINARY); +#endif +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { +#if !defined CC_BUILD_OS2 + return File_Do(file, path, O_RDWR | O_CREAT); +#else + return File_Do(file, path, O_RDWR | O_CREAT | O_BINARY); +#endif +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + *bytesRead = read(file, data, count); + return *bytesRead == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + *bytesWrote = write(file, data, count); + return *bytesWrote == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + return close(file) == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return lseek(file, offset, modes[seekType]) == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = lseek(file, 0, SEEK_CUR); + return *pos == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + struct stat st; + if (fstat(file, &st) == -1) { *len = -1; return errno; } + *len = st.st_size; return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { usleep(milliseconds * 1000); } + +#ifdef CC_BUILD_ANDROID +/* All threads using JNI must detach BEFORE they exit */ +/* (see https://developer.android.com/training/articles/perf-jni#threads */ +static void* ExecThread(void* param) { + JNIEnv* env; + JavaGetCurrentEnv(env); + + ((Thread_StartFunc)param)(); + (*VM_Ptr)->DetachCurrentThread(VM_Ptr); + return NULL; +} +#else +static void* ExecThread(void* param) { + ((Thread_StartFunc)param)(); + return NULL; +} +#endif + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + pthread_t* ptr = (pthread_t*)Mem_Alloc(1, sizeof(pthread_t), "thread"); + int res; + *handle = ptr; + + pthread_attr_t attrs; + pthread_attr_init(&attrs); + pthread_attr_setstacksize(&attrs, stackSize); + + res = pthread_create(ptr, &attrs, ExecThread, (void*)func); + if (res) Logger_Abort2(res, "Creating thread"); + pthread_attr_destroy(&attrs); + +#if defined CC_BUILD_LINUX || defined CC_BUILD_HAIKU + extern int pthread_setname_np(pthread_t thread, const char* name); + pthread_setname_np(*ptr, name); +#elif defined CC_BUILD_FREEBSD || defined CC_BUILD_OPENBSD + extern int pthread_set_name_np(pthread_t thread, const char* name); + pthread_set_name_np(*ptr, name); +#elif defined CC_BUILD_NETBSD + pthread_setname_np(*ptr, "%s", name); +#endif +} + +void Thread_Detach(void* handle) { + pthread_t* ptr = (pthread_t*)handle; + int res = pthread_detach(*ptr); + if (res) Logger_Abort2(res, "Detaching thread"); + Mem_Free(ptr); +} + +void Thread_Join(void* handle) { + pthread_t* ptr = (pthread_t*)handle; + int res = pthread_join(*ptr, NULL); + if (res) Logger_Abort2(res, "Joining thread"); + Mem_Free(ptr); +} + +void* Mutex_Create(const char* name) { + pthread_mutex_t* ptr = (pthread_mutex_t*)Mem_Alloc(1, sizeof(pthread_mutex_t), "mutex"); + int res = pthread_mutex_init(ptr, NULL); + if (res) Logger_Abort2(res, "Creating mutex"); + return ptr; +} + +void Mutex_Free(void* handle) { + int res = pthread_mutex_destroy((pthread_mutex_t*)handle); + if (res) Logger_Abort2(res, "Destroying mutex"); + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + int res = pthread_mutex_lock((pthread_mutex_t*)handle); + if (res) Logger_Abort2(res, "Locking mutex"); +} + +void Mutex_Unlock(void* handle) { + int res = pthread_mutex_unlock((pthread_mutex_t*)handle); + if (res) Logger_Abort2(res, "Unlocking mutex"); +} + +struct WaitData { + pthread_cond_t cond; + pthread_mutex_t mutex; + int signalled; /* For when Waitable_Signal is called before Waitable_Wait */ +}; + +void* Waitable_Create(const char* name) { + struct WaitData* ptr = (struct WaitData*)Mem_Alloc(1, sizeof(struct WaitData), "waitable"); + int res; + + res = pthread_cond_init(&ptr->cond, NULL); + if (res) Logger_Abort2(res, "Creating waitable"); + res = pthread_mutex_init(&ptr->mutex, NULL); + if (res) Logger_Abort2(res, "Creating waitable mutex"); + + ptr->signalled = false; + return ptr; +} + +void Waitable_Free(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + int res; + + res = pthread_cond_destroy(&ptr->cond); + if (res) Logger_Abort2(res, "Destroying waitable"); + res = pthread_mutex_destroy(&ptr->mutex); + if (res) Logger_Abort2(res, "Destroying waitable mutex"); + Mem_Free(handle); +} + +void Waitable_Signal(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + int res; + + Mutex_Lock(&ptr->mutex); + ptr->signalled = true; + Mutex_Unlock(&ptr->mutex); + + res = pthread_cond_signal(&ptr->cond); + if (res) Logger_Abort2(res, "Signalling event"); +} + +void Waitable_Wait(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + int res; + + Mutex_Lock(&ptr->mutex); + if (!ptr->signalled) { + res = pthread_cond_wait(&ptr->cond, &ptr->mutex); + if (res) Logger_Abort2(res, "Waitable wait"); + } + ptr->signalled = false; + Mutex_Unlock(&ptr->mutex); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + struct WaitData* ptr = (struct WaitData*)handle; + struct timeval tv; + struct timespec ts; + int res; + gettimeofday(&tv, NULL); + + /* absolute time for some silly reason */ + ts.tv_sec = tv.tv_sec + milliseconds / 1000; + ts.tv_nsec = 1000 * (tv.tv_usec + 1000 * (milliseconds % 1000)); + + /* statement above might exceed max nsec, so adjust for that */ + while (ts.tv_nsec >= NS_PER_SEC) { + ts.tv_sec++; + ts.tv_nsec -= NS_PER_SEC; + } + + Mutex_Lock(&ptr->mutex); + if (!ptr->signalled) { + res = pthread_cond_timedwait(&ptr->cond, &ptr->mutex, &ts); + if (res && res != ETIMEDOUT) Logger_Abort2(res, "Waitable wait for"); + } + ptr->signalled = false; + Mutex_Unlock(&ptr->mutex); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Font/Text--------------------------------------------------------* +*#########################################################################################################################*/ +static void FontDirCallback(const cc_string* path, void* obj) { + SysFonts_Register(path, NULL); +} + +void Platform_LoadSysFonts(void) { + int i; +#if defined CC_BUILD_ANDROID + static const cc_string dirs[] = { + String_FromConst("/system/fonts"), + String_FromConst("/system/font"), + String_FromConst("/data/fonts"), + }; +#elif defined CC_BUILD_NETBSD + static const cc_string dirs[] = { + String_FromConst("/usr/X11R7/lib/X11/fonts"), + String_FromConst("/usr/pkg/lib/X11/fonts"), + String_FromConst("/usr/pkg/share/fonts") + }; +#elif defined CC_BUILD_OPENBSD + static const cc_string dirs[] = { + String_FromConst("/usr/X11R6/lib/X11/fonts"), + String_FromConst("/usr/share/fonts"), + String_FromConst("/usr/local/share/fonts") + }; +#elif defined CC_BUILD_HAIKU + static const cc_string dirs[] = { + String_FromConst("/system/data/fonts") + }; +#elif defined CC_BUILD_BEOS + static const cc_string dirs[] = { + String_FromConst("/boot/beos/etc/fonts") + }; +#elif defined CC_BUILD_DARWIN + static const cc_string dirs[] = { + String_FromConst("/System/Library/Fonts"), + String_FromConst("/Library/Fonts") + }; +#elif defined CC_BUILD_SERENITY + static const cc_string dirs[] = { + String_FromConst("/res/fonts") + }; +#elif defined CC_BUILD_OS2 + static const cc_string dirs[] = { + String_FromConst("/@unixroot/usr/share/fonts"), + String_FromConst("/@unixroot/usr/local/share/fonts") + }; +#else + static const cc_string dirs[] = { + String_FromConst("/usr/share/fonts"), + String_FromConst("/usr/local/share/fonts") + }; +#endif + for (i = 0; i < Array_Elems(dirs); i++) { + Directory_Enum(&dirs[i], NULL, FontDirCallback); + } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_OS2 +#undef AF_INET6 +#endif + +union SocketAddress { + struct sockaddr raw; + struct sockaddr_in v4; + #ifdef AF_INET6 + struct sockaddr_in6 v6; + struct sockaddr_storage total; + #endif +}; +/* Sanity check to ensure cc_sockaddr struct is large enough to contain all socket addresses supported by this platform */ +static char sockaddr_size_check[sizeof(union SocketAddress) < CC_SOCKETADDR_MAXSIZE ? 1 : -1]; + +static cc_result ParseHost(const char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int res, i = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + res = getaddrinfo(host, portRaw, &hints, &result); + if (res == EAI_AGAIN) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + /* Prefer IPv4 addresses first */ + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family != AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family == AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + union SocketAddress* addr = (union SocketAddress*)addrs[0].data; + char str[NATIVE_STR_LEN]; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + if (inet_pton(AF_INET, str, &addr->v4.sin_addr) > 0) { + addr->v4.sin_family = AF_INET; + addr->v4.sin_port = htons(port); + + addrs[0].size = sizeof(addr->v4); + *numValidAddrs = 1; + return 0; + } + + #ifdef AF_INET6 + if (inet_pton(AF_INET6, str, &addr->v6.sin6_addr) > 0) { + addr->v6.sin6_family = AF_INET6; + addr->v6.sin6_port = htons(port); + + addrs[0].size = sizeof(addr->v6); + *numValidAddrs = 1; + return 0; + } + #endif + + return ParseHost(str, port, addrs, numValidAddrs); +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + cc_result res; + + *s = socket(raw->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (*s == -1) return errno; + + if (nonblocking) { + int blocking_raw = -1; /* non-blocking mode */ + ioctl(*s, FIONBIO, &blocking_raw); + } + + res = connect(*s, raw, addr->size); + return res == -1 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = recv(s, data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return errno; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = send(s, data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + *modified = 0; return errno; +} + +void Socket_Close(cc_socket s) { + shutdown(s, SHUT_RDWR); + close(s); +} + +#if defined CC_BUILD_DARWIN || defined CC_BUILD_BEOS +/* poll is broken on old OSX apparently https://daniel.haxx.se/docs/poll-vs-select.html */ +/* BeOS lacks support for poll */ +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + fd_set set; + struct timeval time = { 0 }; + int selectCount; + + FD_ZERO(&set); + FD_SET(s, &set); + + if (mode == SOCKET_POLL_READ) { + selectCount = select(s + 1, &set, NULL, NULL, &time); + } else { + selectCount = select(s + 1, NULL, &set, NULL, &time); + } + + if (selectCount == -1) { *success = false; return errno; } + *success = FD_ISSET(s, &set) != 0; return 0; +} +#else +#include +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + int flags; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + if (poll(&pfd, 1, 0) == -1) { *success = false; return errno; } + + /* to match select, closed socket still counts as readable */ + flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} +#endif + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Process/Module------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Process_OpenSupported = true; + +#if defined CC_BUILD_MOBILE +cc_result Process_StartGame2(const cc_string* args, int numArgs) { + return SetGameArgs(args, numArgs); +} +#else +static cc_result Process_RawStart(const char* path, char** argv) { + pid_t pid = fork(); + if (pid == -1) return errno; + + if (pid == 0) { + /* Executed in child process */ + execvp(path, argv); + _exit(127); /* "command not found" */ + } else { + /* Executed in parent process */ + /* We do nothing here.. */ + return 0; + } +} + +static cc_result Process_RawGetExePath(char* path, int* len); + +cc_result Process_StartGame2(const cc_string* args, int numArgs) { + char raw[GAME_MAX_CMDARGS][NATIVE_STR_LEN]; + char path[NATIVE_STR_LEN]; + int i, j, len = 0; + char* argv[15]; + cc_result res; + if (Platform_SingleProcess) return SetGameArgs(args, numArgs); + + res = Process_RawGetExePath(path, &len); + if (res) return res; + path[len] = '\0'; + argv[0] = path; + + for (i = 0, j = 1; i < numArgs; i++, j++) + { + String_EncodeUtf8(raw[i], &args[i]); + argv[j] = raw[i]; + } + + argv[j] = NULL; + return Process_RawStart(path, argv); +} +#endif +void Process_Exit(cc_result code) { exit(code); } + +/* Opening browser/starting shell is not really standardised */ +#if defined CC_BUILD_ANDROID +/* Implemented in Platform_Android.c */ +#elif defined CC_BUILD_IOS +/* implemented in interop_ios.m */ +#elif defined CC_BUILD_MACOS +cc_result Process_StartOpen(const cc_string* args) { + UInt8 str[NATIVE_STR_LEN]; + CFURLRef urlCF; + int len; + + len = String_EncodeUtf8(str, args); + urlCF = CFURLCreateWithBytes(kCFAllocatorDefault, str, len, kCFStringEncodingUTF8, NULL); + LSOpenCFURLRef(urlCF, NULL); + CFRelease(urlCF); + return 0; +} +#elif defined CC_BUILD_HAIKU || defined CC_BUILD_BEOS +/* Implemented in interop_BeOS.cpp */ +#elif defined CC_BUILD_OS2 +inline static void ShowErrorMessage(const char *url) { + static char errorMsg[] = "Could not open browser. Please go to: "; + cc_string message = String_Init(errorMsg, strlen(errorMsg), 500); + String_AppendConst(&message, url); + Logger_DialogWarn(&message); +} + +cc_result Process_StartOpen(const cc_string* args) { + char str[NATIVE_STR_LEN]; + APIRET rc; + UCHAR path[CCHMAXPATH], params[100], parambuffer[500], *paramptr; + UCHAR userPath[CCHMAXPATH], sysPath[CCHMAXPATH]; + PRFPROFILE profile = { sizeof(userPath), userPath, sizeof(sysPath), sysPath }; + HINI os2Ini; + HAB hAnchor = WinQueryAnchorBlock(WinQueryActiveWindow(HWND_DESKTOP)); + RESULTCODES result = { 0 }; + PROGDETAILS details; + + // We get URL + String_EncodeUtf8(str, args); + + // Initialize buffers + Mem_Set(path, 0, sizeof(path)); + Mem_Set(parambuffer, 0, sizeof(parambuffer)); + Mem_Set(params, 0, sizeof(params)); + + // We have to look in the OS/2 configuration for the default browser. + // First step: Find the configuration files + if (!PrfQueryProfile(hAnchor, &profile)) { + ShowErrorMessage(str); + return 0; + } + + // Second step: Open the configuration files and read exe path and parameters + os2Ini = PrfOpenProfile(hAnchor, userPath); + if (os2Ini == NULLHANDLE) { + ShowErrorMessage(str); + return 0; + } + if (!PrfQueryProfileString(os2Ini, "WPURLDEFAULTSETTINGS", "DefaultBrowserExe", + NULL, path, sizeof(path))) { + PrfCloseProfile(os2Ini); + ShowErrorMessage(str); + return 0; + } + + PrfQueryProfileString(os2Ini, "WPURLDEFAULTSETTINGS", "DefaultBrowserParameters", + NULL, params, sizeof(params)); + PrfCloseProfile(os2Ini); + + // concat arguments + if (strlen(params) > 0) strncat(params, " ", 20); + strncat(params, str, sizeof(str)); + + // Build parameter buffer + strcpy(parambuffer, "Browser"); + paramptr = ¶mbuffer[strlen(parambuffer)+1]; + // copy params to buffer + strcpy(paramptr, params); + printf("params %p %p %s\n", parambuffer, paramptr, paramptr); + paramptr += strlen(params) + 1; + // To be sure: Terminate parameter list with NULL + *paramptr = '\0'; + + // Last step: Execute detached browser + rc = DosExecPgm(userPath, sizeof(userPath), EXEC_ASYNC, + parambuffer, NULL, &result, path); + if (rc != NO_ERROR) { + ShowErrorMessage(str); + return 0; + } + + return 0; +} +#else +cc_result Process_StartOpen(const cc_string* args) { + char str[NATIVE_STR_LEN]; + char* cmd[3]; + String_EncodeUtf8(str, args); + + /* TODO: Can xdg-open be used on original Solaris, or is it just an OpenIndiana thing */ + cmd[0] = "xdg-open"; cmd[1] = str; cmd[2] = NULL; + Process_RawStart("xdg-open", cmd); + return 0; +} +#endif + +/* Retrieving exe path is completely OS dependant */ +#if defined CC_BUILD_MACOS +static cc_result Process_RawGetExePath(char* path, int* len) { + Mem_Set(path, '\0', NATIVE_STR_LEN); + cc_uint32 size = NATIVE_STR_LEN; + if (_NSGetExecutablePath(path, &size)) return ERR_INVALID_ARGUMENT; + + /* despite what you'd assume, size is NOT changed to length of path */ + *len = String_CalcLen(path, NATIVE_STR_LEN); + return 0; +} +#elif defined CC_BUILD_LINUX || defined CC_BUILD_SERENITY +static cc_result Process_RawGetExePath(char* path, int* len) { + *len = readlink("/proc/self/exe", path, NATIVE_STR_LEN); + return *len == -1 ? errno : 0; +} +#elif defined CC_BUILD_FREEBSD +static cc_result Process_RawGetExePath(char* path, int* len) { + static int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 }; + size_t size = NATIVE_STR_LEN; + + if (sysctl(mib, 4, path, &size, NULL, 0) == -1) return errno; + *len = String_CalcLen(path, NATIVE_STR_LEN); + return 0; +} +#elif defined CC_BUILD_OPENBSD +static cc_result Process_RawGetExePath(char* path, int* len) { + static int mib[4] = { CTL_KERN, KERN_PROC_ARGS, 0, KERN_PROC_ARGV }; + char tmp[NATIVE_STR_LEN]; + size_t size; + char* argv[100]; + char* str; + + /* NOTE: OpenBSD doesn't seem to let us get executable's location, so fallback to argv[0] */ + /* See OpenBSD sysctl manpage for why argv array is so large: */ + /* "... The buffer pointed to by oldp is filled with an array of char pointers followed by the strings themselves..." */ + mib[2] = getpid(); + size = 100 * sizeof(char*); + if (sysctl(mib, 4, argv, &size, NULL, 0) == -1) return errno; + + str = argv[0]; + if (str[0] != '/') { + /* relative path */ + if (!realpath(str, tmp)) return errno; + str = tmp; + } + + *len = String_CalcLen(str, NATIVE_STR_LEN); + Mem_Copy(path, str, *len); + return 0; +} +#elif defined CC_BUILD_NETBSD +static cc_result Process_RawGetExePath(char* path, int* len) { + static int mib[4] = { CTL_KERN, KERN_PROC_ARGS, -1, KERN_PROC_PATHNAME }; + size_t size = NATIVE_STR_LEN; + + if (sysctl(mib, 4, path, &size, NULL, 0) == -1) return errno; + *len = String_CalcLen(path, NATIVE_STR_LEN); + return 0; +} +#elif defined CC_BUILD_SOLARIS +static cc_result Process_RawGetExePath(char* path, int* len) { + *len = readlink("/proc/self/path/a.out", path, NATIVE_STR_LEN); + return *len == -1 ? errno : 0; +} +#elif defined CC_BUILD_HAIKU +static cc_result Process_RawGetExePath(char* path, int* len) { + image_info info; + int32 cookie = 0; + + cc_result res = get_next_image_info(B_CURRENT_TEAM, &cookie, &info); + if (res != B_OK) return res; + + *len = String_CalcLen(info.name, NATIVE_STR_LEN); + Mem_Copy(path, info.name, *len); + return 0; +} +#elif defined CC_BUILD_IRIX +static cc_result Process_RawGetExePath(char* path, int* len) { + static cc_string file = String_FromConst("ClassiCube"); + + /* TODO properly get exe path */ + /* Maybe use PIOCOPENM from https://nixdoc.net/man-pages/IRIX/man4/proc.4.html */ + Mem_Copy(path, file.buffer, file.length); + *len = file.length; + return 0; +} +#elif defined CC_BUILD_OS2 +static cc_result Process_RawGetExePath(char* path, int* len) { + PPIB pib; + DosGetInfoBlocks(NULL, &pib); + if (pib && pib->pib_pchcmd) { + Mem_Copy(path, pib->pib_pchcmd, strlen(pib->pib_pchcmd)); + *len = strlen(pib->pib_pchcmd); + } + return 0; +} +#endif + + +/*########################################################################################################################* +*--------------------------------------------------------Updater----------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_FLATPAK +cc_bool Updater_Supported = false; +#else +cc_bool Updater_Supported = true; +#endif + +#if defined CC_BUILD_ANDROID +/* implemented in Platform_Android.c */ +#elif defined CC_BUILD_IOS +/* implemented in interop_ios.m */ +#else +cc_bool Updater_Clean(void) { return true; } + +#if defined CC_BUILD_RPI + #if __aarch64__ + const struct UpdaterInfo Updater_Info = { "", 1, { { "OpenGL ES", "cc-rpi64" } } }; + #else + const struct UpdaterInfo Updater_Info = { "", 1, { { "OpenGL ES", "ClassiCube.rpi" } } }; + #endif +#elif defined CC_BUILD_LINUX + #if __x86_64__ + const struct UpdaterInfo Updater_Info = { + "&eModernGL is recommended for newer machines (2015 or later)", 2, + { + { "ModernGL", "cc-nix64-gl2" }, + { "OpenGL", "ClassiCube" } + } + }; + #elif __i386__ + const struct UpdaterInfo Updater_Info = { + "&eModernGL is recommended for newer machines (2015 or later)", 2, + { + { "ModernGL", "cc-nix32-gl2" }, + { "OpenGL", "ClassiCube.32" } + } + }; + #else + const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; + #endif +#elif defined CC_BUILD_MACOS + #if __x86_64__ + const struct UpdaterInfo Updater_Info = { + "&eModernGL is recommended for newer machines (2015 or later)", 2, + { + { "ModernGL", "cc-osx64-gl2" }, + { "OpenGL", "ClassiCube.64.osx" } + } + }; + #elif __i386__ + const struct UpdaterInfo Updater_Info = { + "&eModernGL is recommended for newer machines (2015 or later)", 2, + { + { "ModernGL", "cc-osx32-gl2" }, + { "OpenGL", "ClassiCube.osx" } + } + }; + #else + const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; + #endif +#elif defined CC_BUILD_HAIKU + #if __x86_64__ + const struct UpdaterInfo Updater_Info = { "", 1, { { "OpenGL", "cc-haiku-64" } } }; + #else + const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; + #endif +#elif defined CC_BUILD_FREEBSD + #if __x86_64__ + const struct UpdaterInfo Updater_Info = { "", 1, { { "OpenGL", "cc-fbsd64-gl1" } } }; + #elif __i386__ + const struct UpdaterInfo Updater_Info = { "", 1, { { "OpenGL", "cc-fbsd32-gl1" } } }; + #else + const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; + #endif +#elif defined CC_BUILD_NETBSD + #if __x86_64__ + const struct UpdaterInfo Updater_Info = { "", 1, { { "OpenGL", "cc-netbsd64-gl1" } } }; + #else + const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; + #endif +#else + const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; +#endif + +cc_result Updater_Start(const char** action) { + char path[NATIVE_STR_LEN + 1]; + char* argv[2]; + cc_result res; + int len = 0; + + *action = "Getting executable path"; + if ((res = Process_RawGetExePath(path, &len))) return res; + path[len] = '\0'; + + /* Because the process is only referenced by inode, we can */ + /* just unlink current filename and rename updated file to it */ + *action = "Deleting executable"; + if (unlink(path) == -1) return errno; + *action = "Replacing executable"; + if (rename(UPDATE_FILE, path) == -1) return errno; + + argv[0] = path; argv[1] = NULL; + *action = "Restarting game"; + return Process_RawStart(path, argv); +} + +cc_result Updater_GetBuildTime(cc_uint64* timestamp) { + char path[NATIVE_STR_LEN + 1]; + struct stat sb; + int len = 0; + + cc_result res = Process_RawGetExePath(path, &len); + if (res) return res; + path[len] = '\0'; + + if (stat(path, &sb) == -1) return errno; + *timestamp = (cc_uint64)sb.st_mtime; + return 0; +} + +cc_result Updater_MarkExecutable(void) { + struct stat st; + if (stat(UPDATE_FILE, &st) == -1) return errno; + + st.st_mode |= S_IXUSR; + return chmod(UPDATE_FILE, st.st_mode) == -1 ? errno : 0; +} + +cc_result Updater_SetNewBuildTime(cc_uint64 timestamp) { + struct utimbuf times = { 0 }; + times.modtime = timestamp; + return utime(UPDATE_FILE, ×) == -1 ? errno : 0; +} +#endif + + +/*########################################################################################################################* +*-------------------------------------------------------Dynamic lib-------------------------------------------------------* +*#########################################################################################################################*/ +#if defined MAC_OS_X_VERSION_MIN_REQUIRED && (MAC_OS_X_VERSION_MIN_REQUIRED < 1040) +/* Really old mac OS versions don't have the dlopen/dlsym API */ +const cc_string DynamicLib_Ext = String_FromConst(".dylib"); + +void* DynamicLib_Load2(const cc_string* path) { + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, path); + return NSAddImage(str, NSADDIMAGE_OPTION_WITH_SEARCHING | + NSADDIMAGE_OPTION_RETURN_ON_ERROR); +} + +void* DynamicLib_Get2(void* lib, const char* name) { + cc_string tmp; char tmpBuffer[128]; + NSSymbol sym; + String_InitArray_NT(tmp, tmpBuffer); + + /* NS linker api rquires symbols to have a _ prefix */ + String_Append(&tmp, '_'); + String_AppendConst(&tmp, name); + tmp.buffer[tmp.length] = '\0'; + + sym = NSLookupSymbolInImage(lib, tmp.buffer, NSLOOKUPSYMBOLINIMAGE_OPTION_BIND_NOW | + NSLOOKUPSYMBOLINIMAGE_OPTION_RETURN_ON_ERROR); + return sym ? NSAddressOfSymbol(sym) : NULL; +} + +cc_bool DynamicLib_DescribeError(cc_string* dst) { + NSLinkEditErrors err = 0; + const char* name = ""; + const char* msg = ""; + int errNum = 0; + + NSLinkEditError(&err, &errNum, &name, &msg); + String_Format4(dst, "%c in %c (%i, sys %i)", msg, name, &err, &errNum); + return true; +} +#else +#include +/* TODO: Should we use .bundle instead of .dylib? */ + +#ifdef CC_BUILD_DARWIN +const cc_string DynamicLib_Ext = String_FromConst(".dylib"); +#else +const cc_string DynamicLib_Ext = String_FromConst(".so"); +#endif + +void* DynamicLib_Load2(const cc_string* path) { + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, path); + return dlopen(str, RTLD_NOW); +} + +void* DynamicLib_Get2(void* lib, const char* name) { + void *result = dlsym(lib, name); + return result; +} + +cc_bool DynamicLib_DescribeError(cc_string* dst) { + char* err = dlerror(); + if (err) String_AppendConst(dst, err); + return err && err[0]; +} +#endif + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static void Platform_InitPosix(void) { + signal(SIGCHLD, SIG_IGN); + /* So writing to closed socket doesn't raise SIGPIPE */ + signal(SIGPIPE, SIG_IGN); +} +void Platform_Free(void) { } + +#ifdef CC_BUILD_IRIX +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + const char* err = strerror(res); + if (!err || res >= 1000) return false; + + String_AppendUtf8(dst, err, String_Length(err)); + return true; +} +#else +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} +#endif + +#if defined CC_BUILD_DARWIN + +#if defined CC_BUILD_MACOS +static void Platform_InitSpecific(void) { + ProcessSerialNumber psn = { 0, kCurrentProcess }; + #ifdef __ppc__ + /* TransformProcessType doesn't work with kCurrentProcess on older macOS */ + GetCurrentProcess(&psn); + #endif + + /* NOTE: Call as soon as possible, otherwise can't click on dialog boxes or create windows */ + /* NOTE: TransformProcessType is macOS 10.3 or later */ + TransformProcessType(&psn, kProcessTransformToForegroundApplication); +} +#else +static void Platform_InitSpecific(void) { + Platform_SingleProcess = true; + /* Always foreground process on iOS */ +} +#endif + +void Platform_Init(void) { + Stopwatch_Init(); + Platform_InitPosix(); + Platform_InitSpecific(); +} +#else +void Platform_Init(void) { + #ifdef CC_BUILD_MOBILE + Platform_SingleProcess = true; + #endif + + Platform_InitPosix(); +} +#endif + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +/* Encrypts data using XTEA block cipher, with OS specific method to get machine-specific key */ + +static void EncipherBlock(cc_uint32* v, const cc_uint32* key, cc_string* dst) { + cc_uint32 v0 = v[0], v1 = v[1], sum = 0, delta = 0x9E3779B9; + int i; + + for (i = 0; i < 12; i++) { + v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); + sum += delta; + v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]); + } + v[0] = v0; v[1] = v1; + String_AppendAll(dst, v, 8); +} + +static void DecipherBlock(cc_uint32* v, const cc_uint32* key) { + cc_uint32 v0 = v[0], v1 = v[1], delta = 0x9E3779B9, sum = delta * 12; + int i; + + for (i = 0; i < 12; i++) { + v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]); + sum -= delta; + v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]); + } + v[0] = v0; v[1] = v1; +} + +#define ENC1 0xCC005EC0 +#define ENC2 0x0DA4A0DE +#define ENC3 0xC0DED000 +#define MACHINEID_LEN 32 +#define ENC_SIZE 8 /* 2 32 bit ints per block */ + +/* "b3 c5a-0d9" --> 0xB3C5A0D9 */ +static void DecodeMachineID(char* tmp, int len, cc_uint32* key) { + int hex[MACHINEID_LEN] = { 0 }, i, j, c; + cc_uint8* dst = (cc_uint8*)key; + + /* Get each valid hex character */ + for (i = 0, j = 0; i < len && j < MACHINEID_LEN; i++) { + c = PackedCol_DeHex(tmp[i]); + if (c != -1) hex[j++] = c; + } + + for (i = 0; i < MACHINEID_LEN / 2; i++) { + dst[i] = (hex[i * 2] << 4) | hex[i * 2 + 1]; + } +} + +#if defined CC_BUILD_LINUX +/* Read /var/lib/dbus/machine-id or /etc/machine-id for the key */ +static cc_result GetMachineID(cc_uint32* key) { + const cc_string idFile = String_FromConst("/var/lib/dbus/machine-id"); + const cc_string altFile = String_FromConst("/etc/machine-id"); + char tmp[MACHINEID_LEN]; + struct Stream s; + cc_result res; + + /* Some machines only have dbus id, others only have etc id */ + res = Stream_OpenFile(&s, &idFile); + if (res) res = Stream_OpenFile(&s, &altFile); + if (res) return res; + + res = Stream_Read(&s, tmp, MACHINEID_LEN); + if (!res) DecodeMachineID(tmp, MACHINEID_LEN, key); + + (void)s.Close(&s); + return res; +} +#elif defined CC_BUILD_MACOS +/* Read kIOPlatformUUIDKey from I/O registry for the key */ +static cc_result GetMachineID(cc_uint32* key) { + io_registry_entry_t registry; + CFStringRef devID = NULL; + char tmp[256] = { 0 }; + +#ifdef kIOPlatformUUIDKey + registry = IORegistryEntryFromPath(kIOMasterPortDefault, "IOService:/"); + if (!registry) return ERR_NOT_SUPPORTED; + + devID = IORegistryEntryCreateCFProperty(registry, CFSTR(kIOPlatformUUIDKey), kCFAllocatorDefault, 0); + if (devID && CFStringGetCString(devID, tmp, sizeof(tmp), kCFStringEncodingUTF8)) { + DecodeMachineID(tmp, String_Length(tmp), key); + } +#else + registry = IOServiceGetMatchingService(kIOMasterPortDefault, + IOServiceMatching("IOPlatformExpertDevice")); + + devID = IORegistryEntryCreateCFProperty(registry, CFSTR(kIOPlatformSerialNumberKey), kCFAllocatorDefault, 0); + if (devID && CFStringGetCString(devID, tmp, sizeof(tmp), kCFStringEncodingUTF8)) { + Mem_Copy(key, tmp, MACHINEID_LEN / 2); + } +#endif + + if (devID) CFRelease(devID); + IOObjectRelease(registry); + return tmp[0] ? 0 : ERR_NOT_SUPPORTED; +} +#elif defined CC_BUILD_FREEBSD +/* Use kern.hostuuid sysctl for the key */ +/* Possible alternatives: kenv("smbios.system.uuid"), /etc/hostid */ +static cc_result GetMachineID(cc_uint32* key) { + static int mib[2] = { CTL_KERN, KERN_HOSTUUID }; + char buf[128]; + size_t size = 128; + + if (sysctl(mib, 2, buf, &size, NULL, 0) == -1) return errno; + DecodeMachineID(buf, size, key); + return 0; +} +#elif defined CC_BUILD_OPENBSD +/* Use hw.uuid sysctl for the key */ +static cc_result GetMachineID(cc_uint32* key) { + static int mib[2] = { CTL_HW, HW_UUID }; + char buf[128]; + size_t size = 128; + + if (sysctl(mib, 2, buf, &size, NULL, 0) == -1) return errno; + DecodeMachineID(buf, size, key); + return 0; +} +#elif defined CC_BUILD_NETBSD +/* Use hw.uuid for the key */ +static cc_result GetMachineID(cc_uint32* key) { + char buf[128]; + size_t size = 128; + + if (sysctlbyname("machdep.dmi.system-uuid", buf, &size, NULL, 0) == -1) return errno; + DecodeMachineID(buf, size, key); + return 0; +} +#elif defined CC_BUILD_SOLARIS +/* Use SI_HW_SERIAL for the key */ +/* TODO: Should be using SMBIOS UUID for this (search it in illomos source) */ +/* NOTE: Got a '0' for serial number when running in a VM */ +static cc_result GetMachineID(cc_uint32* key) { + char host[HW_HOSTID_LEN] = { 0 }; + if (sysinfo(SI_HW_SERIAL, host, sizeof(host)) == -1) return errno; + + DecodeMachineID(host, HW_HOSTID_LEN, key); + return 0; +} +#elif defined CC_BUILD_ANDROID +static cc_result GetMachineID(cc_uint32* key) { + cc_string dir; char dirBuffer[STRING_SIZE]; + String_InitArray(dir, dirBuffer); + + JavaCall_Void_String("getUUID", &dir); + DecodeMachineID(dirBuffer, dir.length, key); + return 0; +} +#elif defined CC_BUILD_IOS +extern void GetDeviceUUID(cc_string* str); +static cc_result GetMachineID(cc_uint32* key) { + cc_string str; char strBuffer[STRING_SIZE]; + String_InitArray(str, strBuffer); + + GetDeviceUUID(&str); + if (!str.length) return ERR_NOT_SUPPORTED; + + DecodeMachineID(strBuffer, str.length, key); + return 0; +} +#else +static cc_result GetMachineID(cc_uint32* key) { return ERR_NOT_SUPPORTED; } +#endif + +cc_result Platform_Encrypt(const void* data, int len, cc_string* dst) { + const cc_uint8* src = (const cc_uint8*)data; + cc_uint32 header[4], key[4]; + cc_result res; + if ((res = GetMachineID(key))) return res; + + header[0] = ENC1; header[1] = ENC2; + header[2] = ENC3; header[3] = len; + EncipherBlock(header + 0, key, dst); + EncipherBlock(header + 2, key, dst); + + for (; len > 0; len -= ENC_SIZE, src += ENC_SIZE) { + header[0] = 0; header[1] = 0; + Mem_Copy(header, src, min(len, ENC_SIZE)); + EncipherBlock(header, key, dst); + } + return 0; +} +cc_result Platform_Decrypt(const void* data, int len, cc_string* dst) { + const cc_uint8* src = (const cc_uint8*)data; + cc_uint32 header[4], key[4]; + cc_result res; + int dataLen; + + /* Total size must be >= header size */ + if (len < 16) return ERR_END_OF_STREAM; + if ((res = GetMachineID(key))) return res; + + Mem_Copy(header, src, 16); + DecipherBlock(header + 0, key); + DecipherBlock(header + 2, key); + + if (header[0] != ENC1 || header[1] != ENC2 || header[2] != ENC3) return ERR_INVALID_ARGUMENT; + len -= 16; src += 16; + + if (header[3] > len) return ERR_INVALID_ARGUMENT; + dataLen = header[3]; + + for (; dataLen > 0; len -= ENC_SIZE, src += ENC_SIZE, dataLen -= ENC_SIZE) { + header[0] = 0; header[1] = 0; + Mem_Copy(header, src, min(len, ENC_SIZE)); + + DecipherBlock(header, key); + String_AppendAll(dst, header, min(dataLen, ENC_SIZE)); + } + return 0; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Configuration-------------------------------------------------------* +*#########################################################################################################################*/ +#if defined CC_BUILD_MOBILE +int Platform_GetCommandLineArgs(int argc, STRING_REF char** argv, cc_string* args) { + return GetGameArgs(args); +} +#else +int Platform_GetCommandLineArgs(int argc, STRING_REF char** argv, cc_string* args) { + int i, count; + argc--; argv++; /* skip executable path argument */ + if (Platform_SingleProcess) return GetGameArgs(args); + + #if defined CC_BUILD_MACOS + /* Sometimes a "-psn_0_[number]" argument is added before actual args */ + if (argc) { + static const cc_string psn = String_FromConst("-psn_0_"); + cc_string arg0 = String_FromReadonly(argv[0]); + if (String_CaselessStarts(&arg0, &psn)) { argc--; argv++; } + } + #endif + + count = min(argc, GAME_MAX_CMDARGS); + for (i = 0; i < count; i++) + { + /* -d[directory] argument used to change directory data is stored in */ + if (argv[i][0] == '-' && argv[i][1] == 'd' && argv[i][2]) { + Logger_Abort("-d argument no longer supported - cd to desired working directory instead"); + continue; + } + args[i] = String_FromReadonly(argv[i]); + } + return count; +} + +/* Detects if the game is running in $HOME directory */ +static cc_bool IsProblematicWorkingDirectory(void) { + #ifdef CC_BUILD_MACOS + /* TODO: Only change working directory when necessary */ + /* When running from bundle, working directory is "/" */ + return true; + #else + cc_string curDir, homeDir; + char path[2048] = { 0 }; + const char* home; + + getcwd(path, 2048); + curDir = String_FromReadonly(path); + + home = getenv("HOME"); + if (!home) return false; + homeDir = String_FromReadonly(home); + + if (String_Equals(&curDir, &homeDir)) { + Platform_LogConst("Working directory is $HOME! Changing to executable directory.."); + return true; + } + return false; + #endif +} + +cc_result Platform_SetDefaultCurrentDirectory(int argc, char **argv) { + char path[NATIVE_STR_LEN]; + int i, len = 0; + cc_result res; + if (!IsProblematicWorkingDirectory()) return 0; + + res = Process_RawGetExePath(path, &len); + if (res) return res; + + /* get rid of filename at end of directory */ + for (i = len - 1; i >= 0; i--, len--) { + if (path[i] == '/') break; + } + + #ifdef CC_BUILD_MACOS + static const cc_string bundle = String_FromConst(".app/Contents/MacOS/"); + cc_string raw = String_Init(path, len, 0); + + /* If running from within a bundle, set data folder to folder containing bundle */ + if (String_CaselessEnds(&raw, &bundle)) { + len -= bundle.length; + + for (i = len - 1; i >= 0; i--, len--) { + if (path[i] == '/') break; + } + } + #endif + + path[len] = '\0'; + return chdir(path) == -1 ? errno : 0; +} +#endif +#endif diff --git a/src/Platform_Saturn.c b/src/Platform_Saturn.c new file mode 100644 index 0000000..8f96f3b --- /dev/null +++ b/src/Platform_Saturn.c @@ -0,0 +1,240 @@ +#include "Core.h" +#if defined PLAT_SATURN + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "Options.h" +#include "PackedCol.h" +#include +#include +#include +#include + +void* calloc(size_t num, size_t size) { + void* ptr = malloc(num * size); + if (ptr) memset(ptr, 0, num * size); + return ptr; +} +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = 99999; +const cc_result ReturnCode_DirectoryExists = 99999; + +const cc_result ReturnCode_SocketInProgess = -1; +const cc_result ReturnCode_SocketWouldBlock = -1; +const char* Platform_AppNameSuffix = " Saturn"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +#define WRITE_ADDRESS CS0(0x00100001) +// in Medafen, patch DummyWrite in src/ss/Cart.cpp to instead log the value + +void Platform_Log(const char* msg, int len) { + for (int i = 0; i < len; i++) { + MEMORY_WRITE(8, WRITE_ADDRESS, msg[i]); + } + MEMORY_WRITE(8, WRITE_ADDRESS, '\n'); +} + +TimeMS DateTime_CurrentUTC(void) { + return 0; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + Mem_Set(t, 0, sizeof(struct DateTime)); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +static volatile cc_uint32 overflow_count; + +cc_uint64 Stopwatch_Measure(void) { + return cpu_frt_count_get() + (overflow_count * 65536); +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + cc_uint32 delta = end - beg; + + // TODO still wrong?? and overflows?? and PAL detection ??? + return (delta * 1000) / CPU_FRT_NTSC_320_128_COUNT_1MS; +} + +static void ovf_handler(void) { overflow_count++; } + +static void Stopwatch_Init(void) { + //cpu_frt_init(CPU_FRT_CLOCK_DIV_8); + cpu_frt_init(CPU_FRT_CLOCK_DIV_128); + cpu_frt_ovi_set(ovf_handler); + + cpu_frt_interrupt_priority_set(15); + cpu_frt_count_set(0); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Directory_Create(const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +int File_Exists(const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Create(cc_file* file, const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Close(cc_file file) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + return ERR_NOT_SUPPORTED; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + // TODO sleep a bit +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + *handle = NULL; +} + +void Thread_Detach(void* handle) { +} + +void Thread_Join(void* handle) { +} + +void* Mutex_Create(const char* name) { + return NULL; +} + +void Mutex_Free(void* handle) { +} + +void Mutex_Lock(void* handle) { +} + +void Mutex_Unlock(void* handle) { +} + +void* Waitable_Create(const char* name) { + return NULL; +} + +void Waitable_Free(void* handle) { +} + +void Waitable_Signal(void* handle) { +} + +void Waitable_Wait(void* handle) { +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + return ERR_NOT_SUPPORTED; +} + +void Socket_Close(cc_socket s) { +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return ERR_NOT_SUPPORTED; +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Init(void) { + Stopwatch_Init(); +} + +void Platform_Free(void) { } + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_Switch.c b/src/Platform_Switch.c new file mode 100644 index 0000000..009a82b --- /dev/null +++ b/src/Platform_Switch.c @@ -0,0 +1,538 @@ +#include "Core.h" +#if defined CC_BUILD_SWITCH +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "Options.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; // not used +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; +const char* Platform_AppNameSuffix = " Switch"; + +alignas(16) u8 __nx_exception_stack[0x1000]; +u64 __nx_exception_stack_size = sizeof(__nx_exception_stack); + +void __libnx_exception_handler(ThreadExceptionDump *ctx) +{ + int i; + FILE *f = fopen("sdmc:/exception_dump", "w"); + if(f==NULL)return; + + fprintf(f, "error_desc: 0x%x\n", ctx->error_desc);//You can also parse this with ThreadExceptionDesc. + //This assumes AArch64, however you can also use threadExceptionIsAArch64(). + for(i=0; i<29; i++)fprintf(f, "[X%d]: 0x%lx\n", i, ctx->cpu_gprs[i].x); + fprintf(f, "fp: 0x%lx\n", ctx->fp.x); + fprintf(f, "lr: 0x%lx\n", ctx->lr.x); + fprintf(f, "sp: 0x%lx\n", ctx->sp.x); + fprintf(f, "pc: 0x%lx\n", ctx->pc.x); + + //You could print fpu_gprs if you want. + + fprintf(f, "pstate: 0x%x\n", ctx->pstate); + fprintf(f, "afsr0: 0x%x\n", ctx->afsr0); + fprintf(f, "afsr1: 0x%x\n", ctx->afsr1); + fprintf(f, "esr: 0x%x\n", ctx->esr); + + fprintf(f, "far: 0x%lx\n", ctx->far.x); + + fclose(f); +} + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + + // See include/switch/arm/counter.h + // static inline u64 armTicksToNs(u64 tick) { return (tick * 625) / 12; } + return ((end - beg) * 625) / 12000; +} + +cc_uint64 Stopwatch_Measure(void) { + return armGetSystemTick(); +} + +void Platform_Log(const char* msg, int len) { + svcOutputDebugString(msg, len); +} + +TimeMS DateTime_CurrentUTC(void) { + u64 timestamp = 0; + timeGetCurrentTime(TimeType_Default, ×tamp); + return timestamp + UNIX_EPOCH_SECONDS; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + u64 timestamp = 0; + TimeCalendarTime calTime = { 0 }; + timeGetCurrentTime(TimeType_Default, ×tamp); + timeToCalendarTimeWithMyRule(timestamp, &calTime, NULL); + + t->year = calTime.year; + t->month = calTime.month; + t->day = calTime.day; + t->hour = calTime.hour; + t->minute = calTime.minute; + t->second = calTime.second; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static const cc_string root_path = String_FromConst("sdmc:/switch/ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + return mkdir(str, 0) == -1 ? errno : 0; +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + struct stat sb; + GetNativePath(str, path); + return stat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + struct dirent* entry; + int res; + + GetNativePath(str, dirPath); + DIR* dirPtr = opendir(str); + if (!dirPtr) return errno; + + // POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." + // errno is sometimes leftover from previous calls, so always reset it before readdir gets called + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + // ignore . and .. entry + char* src = entry->d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + int len = String_Length(src); + String_AppendUtf8(&path, src, len); + int is_dir = entry->d_type == DT_DIR; + // TODO: fallback to stat when this fails + + if (is_dir) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; // return code from readdir + closedir(dirPtr); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + *file = open(str, mode, 0); + return *file == -1 ? errno : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + *bytesRead = read(file, data, count); + return *bytesRead == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + *bytesWrote = write(file, data, count); + return *bytesWrote == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + return close(file) == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return lseek(file, offset, modes[seekType]) == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = lseek(file, 0, SEEK_CUR); + return *pos == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + struct stat st; + if (fstat(file, &st) == -1) { *len = -1; return errno; } + *len = st.st_size; return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + cc_uint64 timeout_ns = (cc_uint64)milliseconds * (1000 * 1000); // to nanoseconds + svcSleepThread(timeout_ns); +} + +static void ExecSwitchThread(void* param) { + ((Thread_StartFunc)param)(); +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + Thread* thread = (Thread*)Mem_Alloc(1, sizeof(Thread), name); + *handle = thread; + + threadCreate(thread, ExecSwitchThread, (void*)func, NULL, stackSize, 0x2C, -2); + threadStart(thread); +} + +void Thread_Detach(void* handle) { + // threadClose frees up resources, **including the stack of the thread** + // Which obviously completely breaks the thread - so instead just accept + // that there will be a small memory leak when non-joined threads exit +} + +void Thread_Join(void* handle) { + Thread* thread = (Thread*)handle; + threadWaitForExit(thread); + threadClose(thread); + Mem_Free(thread); +} + +void* Mutex_Create(const char* name) { + Mutex* mutex = (Mutex*)Mem_Alloc(1, sizeof(Mutex), "mutex"); + mutexInit(mutex); + return mutex; +} + +void Mutex_Free(void* handle) { + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + mutexLock((Mutex*)handle); +} + +void Mutex_Unlock(void* handle) { + mutexUnlock((Mutex*)handle); +} + + +struct WaitData { + CondVar cond; + Mutex mutex; + int signalled; // For when Waitable_Signal is called before Waitable_Wait +}; + +void* Waitable_Create(const char* name) { + struct WaitData* ptr = (struct WaitData*)Mem_Alloc(1, sizeof(struct WaitData), "waitable"); + + mutexInit(&ptr->mutex); + condvarInit(&ptr->cond); + + ptr->signalled = false; + return ptr; +} + +void Waitable_Free(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + Mem_Free(ptr); +} + +void Waitable_Signal(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + + Mutex_Lock(&ptr->mutex); + condvarWakeOne(&ptr->cond); + Mutex_Unlock(&ptr->mutex); + + ptr->signalled = true; +} + +void Waitable_Wait(void* handle) { + struct WaitData* ptr = (struct WaitData*)handle; + + Mutex_Lock(&ptr->mutex); + if (!ptr->signalled) { + condvarWait(&ptr->cond, &ptr->mutex); + } + ptr->signalled = false; + Mutex_Unlock(&ptr->mutex); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + struct WaitData* ptr = (struct WaitData*)handle; + cc_uint64 timeout_ns = (cc_uint64)milliseconds * (1000 * 1000); // to nanoseconds + + Mutex_Lock(&ptr->mutex); + if (!ptr->signalled) { + condvarWaitTimeout(&ptr->cond, &ptr->mutex, timeout_ns); + } + ptr->signalled = false; + Mutex_Unlock(&ptr->mutex); +} +/* + +void* Waitable_Create(const char* name) { + LEvent* ptr = (LEvent*)Mem_Alloc(1, sizeof(LEvent), "waitable"); + leventInit(ptr, false, true); + return ptr; +} + +void Waitable_Free(void* handle) { + LEvent* ptr = (LEvent*)handle; + leventClear(ptr); + Mem_Free(ptr); +} + +void Waitable_Signal(void* handle) { + //leventSignal((LEvent*)handle); +} + +void Waitable_Wait(void* handle) { + leventWait((LEvent*)handle, UINT64_MAX); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + cc_uint64 timeout_ns = milliseconds * (1000 * 1000); // to nanoseconds + leventWait((LEvent*)handle, timeout_ns); +} +*/ + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +union SocketAddress { + struct sockaddr raw; + struct sockaddr_in v4; + struct sockaddr_in6 v6; + struct sockaddr_storage total; +}; + +static cc_result ParseHost(const char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int res, i = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + res = getaddrinfo(host, portRaw, &hints, &result); + if (res == EAI_AGAIN) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + /* Prefer IPv4 addresses first */ + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family != AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family == AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + union SocketAddress* addr = (union SocketAddress*)addrs[0].data; + char str[NATIVE_STR_LEN]; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + if (inet_pton(AF_INET, str, &addr->v4.sin_addr) > 0) { + addr->v4.sin_family = AF_INET; + addr->v4.sin_port = htons(port); + + addrs[0].size = sizeof(addr->v4); + *numValidAddrs = 1; + return 0; + } + + if (inet_pton(AF_INET6, str, &addr->v6.sin6_addr) > 0) { + addr->v6.sin6_family = AF_INET6; + addr->v6.sin6_port = htons(port); + + addrs[0].size = sizeof(addr->v6); + *numValidAddrs = 1; + return 0; + } + + return ParseHost(str, port, addrs, numValidAddrs); +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + cc_result res; + + *s = socket(raw->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (*s == -1) return errno; + + if (nonblocking) { + fcntl(*s, F_SETFL, O_NONBLOCK); + } + + res = connect(*s, raw, addr->size); + return res == -1 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = recv(s, data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return errno; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = send(s, data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + *modified = 0; return errno; +} + +void Socket_Close(cc_socket s) { + shutdown(s, SHUT_RDWR); + close(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + int flags; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + if (poll(&pfd, 1, 0) == -1) { *success = false; return errno; } + + /* to match select, closed socket still counts as readable */ + flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static void CreateRootDirectory(void) { + mkdir("sdmc:/switch", 0); + int res = mkdir(root_path.buffer, 0); + int err = res == -1 ? errno : 0; + Platform_Log1("Created root directory: %i", &err); +} + +void Platform_Init(void) { + CreateRootDirectory(); + socketInitializeDefault(); +} + +void Platform_Free(void) { + socketExit(); +} + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_Web.c b/src/Platform_Web.c new file mode 100644 index 0000000..c0a73c5 --- /dev/null +++ b/src/Platform_Web.c @@ -0,0 +1,438 @@ +#include "Core.h" +#if defined CC_BUILD_WEB + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "SystemFonts.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" + +#include +#include +#include +#include + +#define O_RDONLY 0x000 +#define O_WRONLY 0x001 +#define O_RDWR 0x002 +#define O_CREAT 0x040 +#define O_EXCL 0x080 +#define O_TRUNC 0x200 + +/* Unfortunately, errno constants are different in some older emscripten versions */ +/* (linux errno numbers compared to WASI errno numbers) */ +/* So just use the same errono numbers as interop_web.js */ +#define _ENOENT 2 +#define _EAGAIN 6 /* same as EWOULDBLOCK */ +#define _EEXIST 17 +#define _EHOSTUNREACH 23 +#define _EINPROGRESS 26 + +const cc_result ReturnCode_FileShareViolation = 1000000000; /* Not used in web filesystem backend */ +const cc_result ReturnCode_FileNotFound = _ENOENT; +const cc_result ReturnCode_SocketInProgess = _EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = _EAGAIN; +const cc_result ReturnCode_DirectoryExists = _EEXIST; +const char* Platform_AppNameSuffix = ""; +cc_bool Platform_SingleProcess; + + +/*########################################################################################################################* +*---------------------------------------------------------Memory----------------------------------------------------------* +*#########################################################################################################################*/ +void* Mem_Set(void* dst, cc_uint8 value, cc_uint32 numBytes) { return memset( dst, value, numBytes); } +void* Mem_Copy(void* dst, const void* src, cc_uint32 numBytes) { return memcpy( dst, src, numBytes); } +void* Mem_Move(void* dst, const void* src, cc_uint32 numBytes) { return memmove(dst, src, numBytes); } + +void* Mem_TryAlloc(cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? malloc(size) : NULL; +} + +void* Mem_TryAllocCleared(cc_uint32 numElems, cc_uint32 elemsSize) { + return calloc(numElems, elemsSize); +} + +void* Mem_TryRealloc(void* mem, cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? realloc(mem, size) : NULL; +} + +void Mem_Free(void* mem) { + if (mem) free(mem); +} + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return end - beg; +} + +cc_uint64 Stopwatch_Measure(void) { + /* time is a milliseconds double */ + /* convert to microseconds */ + return (cc_uint64)(emscripten_get_now() * 1000); +} + +extern void interop_Log(const char* msg, int len); +void Platform_Log(const char* msg, int len) { + interop_Log(msg, len); +} + +TimeMS DateTime_CurrentUTC(void) { + struct timeval cur; + gettimeofday(&cur, NULL); + return (cc_uint64)cur.tv_sec + UNIX_EPOCH_SECONDS; +} + +extern void interop_GetLocalTime(struct DateTime* t); +void DateTime_CurrentLocal(struct DateTime* t) { + interop_GetLocalTime(t); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +void Directory_GetCachePath(cc_string* path) { } + +extern void interop_InitFilesystem(void); +cc_result Directory_Create(const cc_string* path) { + /* Web filesystem doesn't need directories */ + return 0; +} + +extern int interop_FileExists(const char* path); +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, path); + return interop_FileExists(str); +} + +static void* enum_obj; +static Directory_EnumCallback enum_callback; +EMSCRIPTEN_KEEPALIVE void Directory_IterCallback(const char* src) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + + String_InitArray(path, pathBuffer); + String_AppendUtf8(&path, src, String_Length(src)); + enum_callback(&path, enum_obj); +} + +extern int interop_DirectoryIter(const char* path); +cc_result Directory_Enum(const cc_string* path, void* obj, Directory_EnumCallback callback) { + char str[NATIVE_STR_LEN]; + String_EncodeUtf8(str, path); + + enum_obj = obj; + enum_callback = callback; + /* returned result is negative for error */ + return -interop_DirectoryIter(str); +} + +extern int interop_FileCreate(const char* path, int mode); +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + int res; + String_EncodeUtf8(str, path); + res = interop_FileCreate(str, mode); + + /* returned result is negative for error */ + if (res >= 0) { + *file = res; return 0; + } else { + *file = -1; return -res; + } +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT); +} + +extern int interop_FileRead(int fd, void* data, int count); +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + int res = interop_FileRead(file, data, count); + + /* returned result is negative for error */ + if (res >= 0) { + *bytesRead = res; return 0; + } else { + *bytesRead = -1; return -res; + } +} + +extern int interop_FileWrite(int fd, const void* data, int count); +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + int res = interop_FileWrite(file, data, count); + + /* returned result is negative for error */ + if (res >= 0) { + *bytesWrote = res; return 0; + } else { + *bytesWrote = -1; return -res; + } +} + +extern int interop_FileClose(int fd); +cc_result File_Close(cc_file file) { + /* returned result is negative for error */ + return -interop_FileClose(file); +} + +extern int interop_FileSeek(int fd, int offset, int whence); +cc_result File_Seek(cc_file file, int offset, int seekType) { + /* returned result is negative for error */ + int res = interop_FileSeek(file, offset, seekType); + /* FileSeek returns current position, discard that */ + return res >= 0 ? 0 : -res; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + /* FILE_SEEKFROM_CURRENT is same as SEEK_CUR */ + int res = interop_FileSeek(file, 0, FILE_SEEKFROM_CURRENT); + /* returned result is negative for error */ + if (res >= 0) { + *pos = res; return 0; + } else { + *pos = -1; return -res; + } +} + +extern int interop_FileLength(int fd); +cc_result File_Length(cc_file file, cc_uint32* len) { + int res = interop_FileLength(file); + /* returned result is negative for error */ + if (res >= 0) { + *len = res; return 0; + } else { + *len = -1; return -res; + } +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +/* No real threading support with emscripten backend */ +void Thread_Sleep(cc_uint32 milliseconds) { } + +void* Mutex_Create(const char* name) { return NULL; } +void Mutex_Free(void* handle) { } +void Mutex_Lock(void* handle) { } +void Mutex_Unlock(void* handle) { } + +void* Waitable_Create(const char* name) { return NULL; } +void Waitable_Free(void* handle) { } +void Waitable_Signal(void* handle) { } +void Waitable_Wait(void* handle) { } +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { } + + +/*########################################################################################################################* +*--------------------------------------------------------Font/Text--------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_LoadSysFonts(void) { } + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +extern void interop_InitSockets(void); + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + int len = String_EncodeUtf8(addrs[0].data, address); + /* TODO can this ever happen */ + if (len >= CC_SOCKETADDR_MAXSIZE) Logger_Abort("Overrun in Socket_ParseAddress"); + + addrs[0].size = port; + *numValidAddrs = 1; + return 0; +} + +extern int interop_SocketCreate(void); +extern int interop_SocketConnect(int sock, const cc_uint8* host, int port); +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + int res; + + *s = interop_SocketCreate(); + /* size is used to store port number instead */ + /* returned result is negative for error */ + res = -interop_SocketConnect(*s, addr->data, addr->size); + + /* error returned when invalid address provided */ + if (res == _EHOSTUNREACH) return ERR_INVALID_ARGUMENT; + return res; +} + +extern int interop_SocketRecv(int sock, void* data, int len); +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* read) { + int res; + *read = 0; + + /* interop_SocketRecv only reads one WebSocket frame at most, hence call it multiple times */ + while (count) { + /* returned result is negative for error */ + res = interop_SocketRecv(s, data, count); + + if (res >= 0) { + *read += res; + data += res; count -= res; + } else { + /* EAGAIN when no more data available */ + if (res == -_EAGAIN) return *read == 0 ? _EAGAIN : 0; + + return -res; + } + } + return 0; +} + +extern int interop_SocketSend(int sock, const void* data, int len); +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + /* returned result is negative for error */ + int res = interop_SocketSend(s, data, count); + + if (res >= 0) { + *modified = res; return 0; + } else { + *modified = 0; return -res; + } +} + +extern int interop_SocketClose(int sock); +void Socket_Close(cc_socket s) { + interop_SocketClose(s); +} + +extern int interop_SocketWritable(int sock, cc_bool* writable); +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + /* returned result is negative for error */ + return -interop_SocketWritable(s, writable); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Process/Module------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Process_OpenSupported = true; + +void Process_Exit(cc_result code) { + /* 'Window' (i.e. the web canvas) isn't implicitly closed when process is exited */ + if (code) Window_RequestClose(); + /* game normally calls exit with code = 0 due to async IndexedDB loading */ + if (code) exit(code); +} + +extern int interop_OpenTab(const char* url); +cc_result Process_StartOpen(const cc_string* args) { + char str[NATIVE_STR_LEN]; + cc_result res; + String_EncodeUtf8(str, args); + + res = interop_OpenTab(str); + /* interop error code -> ClassiCube error code */ + if (res == 1) res = ERR_INVALID_OPEN_URL; + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char* str; + int len; + + /* For unrecognised error codes, strerror might return messages */ + /* such as 'No error information', which is not very useful */ + if (res >= 1000) return false; + + str = strerror(res); + if (!str) return false; + + len = String_CalcLen(str, NATIVE_STR_LEN); + String_AppendUtf8(dst, str, len); + return true; +} + +EMSCRIPTEN_KEEPALIVE void Platform_LogError(const char* msg) { + cc_string str = String_FromReadonly(msg); + Logger_WarnFunc(&str); +} + +extern void interop_InitModule(void); +void Platform_Init(void) { + interop_InitModule(); + interop_InitFilesystem(); + interop_InitSockets(); +} +void Platform_Free(void) { } + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Platform_Encrypt(const void* data, int len, cc_string* dst) { return ERR_NOT_SUPPORTED; } +cc_result Platform_Decrypt(const void* data, int len, cc_string* dst) { return ERR_NOT_SUPPORTED; } + + +/*########################################################################################################################* +*------------------------------------------------------Main driver--------------------------------------------------------* +*#########################################################################################################################*/ +int Platform_GetCommandLineArgs(int argc, STRING_REF char** argv, cc_string* args) { + int i, count; + argc--; argv++; /* skip executable path argument */ + + count = min(argc, GAME_MAX_CMDARGS); + for (i = 0; i < count; i++) { args[i] = String_FromReadonly(argv[i]); } + return count; +} + + +cc_result Platform_SetDefaultCurrentDirectory(int argc, char** argv) { return 0; } +static int _argc; +static char** _argv; + +extern void interop_FS_Init(void); +extern void interop_DirectorySetWorking(const char* path); +extern void interop_AsyncDownloadTexturePack(const char* path); + +int main(int argc, char** argv) { + _argc = argc; _argv = argv; + + /* Game loads resources asynchronously, then actually starts itself */ + /* main */ + /* > texture pack download (async) */ + /* > load indexedDB (async) */ + /* > web_main (game actually starts) */ + interop_FS_Init(); + interop_DirectorySetWorking("/classicube"); + interop_AsyncDownloadTexturePack("texpacks/default.zip"); +} + +extern void interop_LoadIndexedDB(void); +extern void interop_AsyncLoadIndexedDB(void); +/* Asynchronous callback after texture pack is downloaded */ +EMSCRIPTEN_KEEPALIVE void main_phase1(void) { + interop_LoadIndexedDB(); /* legacy compatibility */ + interop_AsyncLoadIndexedDB(); +} + +extern int web_main(int argc, char** argv); +/* Asynchronous callback after IndexedDB is loaded */ +EMSCRIPTEN_KEEPALIVE void main_phase2(void) { + web_main(_argc, _argv); +} +#endif diff --git a/src/Platform_WiiU.c b/src/Platform_WiiU.c new file mode 100644 index 0000000..acbd7b6 --- /dev/null +++ b/src/Platform_WiiU.c @@ -0,0 +1,463 @@ +#include "Core.h" +#if defined CC_BUILD_WIIU + +#include "_PlatformBase.h" +#include "Stream.h" +#include "ExtMath.h" +#include "SystemFonts.h" +#include "Funcs.h" +#include "Window.h" +#include "Utils.h" +#include "Errors.h" +#include "PackedCol.h" +#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 +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = 1000000000; /* TODO: not used apparently */ +const cc_result ReturnCode_FileNotFound = ENOENT; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = EEXIST; + +const char* Platform_AppNameSuffix = " Wii U"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Log(const char* msg, int len) { + char tmp[256 + 1]; + len = min(len, 256); + Mem_Copy(tmp, msg, len); tmp[len] = '\0'; + + OSReport("%s\n", tmp); +} +#define WIIU_EPOCH_ADJUST 946684800ULL // Wii U time epoch is year 2000, not 1970 + +TimeMS DateTime_CurrentUTC(void) { + OSTime time = OSGetTime(); + cc_int64 secs = (time_t)OSTicksToSeconds(time); + return secs + UNIX_EPOCH_SECONDS + WIIU_EPOCH_ADJUST; +} + +void DateTime_CurrentLocal(struct DateTime* t) { + struct OSCalendarTime loc_time; + OSTicksToCalendarTime(OSGetTime(), &loc_time); + + t->year = loc_time.tm_year; + t->month = loc_time.tm_mon + 1; + t->day = loc_time.tm_mday; + t->hour = loc_time.tm_hour; + t->minute = loc_time.tm_min; + t->second = loc_time.tm_sec; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Stopwatch--------------------------------------------------------* +*#########################################################################################################################*/ +cc_uint64 Stopwatch_Measure(void) { + return OSGetSystemTime(); // TODO OSGetSystemTick ?? +} + +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return OSTicksToMicroseconds(end - beg); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +// fs:/vol/external01 +//const char* sd_root = WHBGetSdCardMountPath(); + //int sd_length = String_Length(sd_root); + //Mem_Copy(str, sd_root, sd_length); + //str += sd_length; + +static const cc_string root_path = String_FromConst("ClassiCube/"); + +static void GetNativePath(char* str, const cc_string* path) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + String_EncodeUtf8(str, path); +} + +cc_result Directory_Create(const cc_string* path) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + /* read/write/search permissions for owner and group, and with read/search permissions for others. */ + /* TODO: Is the default mode in all cases */ + return mkdir(str, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == -1 ? errno : 0; +} + +int File_Exists(const cc_string* path) { + char str[NATIVE_STR_LEN]; + struct stat sb; + GetNativePath(str, path); + return stat(str, &sb) == 0 && S_ISREG(sb.st_mode); +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[FILENAME_SIZE]; + char str[NATIVE_STR_LEN]; + DIR* dirPtr; + struct dirent* entry; + char* src; + int len, res, is_dir; + + GetNativePath(str, dirPath); + dirPtr = opendir(str); + if (!dirPtr) return errno; + + /* POSIX docs: "When the end of the directory is encountered, a null pointer is returned and errno is not changed." */ + /* errno is sometimes leftover from previous calls, so always reset it before readdir gets called */ + errno = 0; + String_InitArray(path, pathBuffer); + + while ((entry = readdir(dirPtr))) { + path.length = 0; + String_Format1(&path, "%s/", dirPath); + + /* ignore . and .. entry */ + src = entry->d_name; + if (src[0] == '.' && src[1] == '\0') continue; + if (src[0] == '.' && src[1] == '.' && src[2] == '\0') continue; + + len = String_Length(src); + String_AppendUtf8(&path, src, len); + + is_dir = entry->d_type == DT_DIR; + /* TODO: fallback to stat when this fails */ + + if (is_dir) { + res = Directory_Enum(&path, obj, callback); + if (res) { closedir(dirPtr); return res; } + } else { + callback(&path, obj); + } + errno = 0; + } + + res = errno; /* return code from readdir */ + closedir(dirPtr); + return res; +} + +static cc_result File_Do(cc_file* file, const cc_string* path, int mode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + *file = open(str, mode, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + return *file == -1 ? errno : 0; +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDONLY); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT | O_TRUNC); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return File_Do(file, path, O_RDWR | O_CREAT); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + *bytesRead = read(file, data, count); + return *bytesRead == -1 ? errno : 0; +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + *bytesWrote = write(file, data, count); + return *bytesWrote == -1 ? errno : 0; +} + +cc_result File_Close(cc_file file) { + return close(file) == -1 ? errno : 0; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[] = { SEEK_SET, SEEK_CUR, SEEK_END }; + return lseek(file, offset, modes[seekType]) == -1 ? errno : 0; +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = lseek(file, 0, SEEK_CUR); + return *pos == -1 ? errno : 0; +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + struct stat st; + if (fstat(file, &st) == -1) { *len = -1; return errno; } + *len = st.st_size; return 0; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#########################################################################################################################*/ +void Thread_Sleep(cc_uint32 milliseconds) { + OSSleepTicks(OSMillisecondsToTicks(milliseconds)); +} + +static int ExecThread(int argc, const char **argv) { + ((Thread_StartFunc)argv)(); + return 0; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + OSThread* thread = (OSThread*)Mem_Alloc(1, sizeof(OSThread), "thread"); + void* stack = memalign(16, stackSize); + + OSCreateThread(thread, ExecThread, + 1, (Thread_StartFunc)func, + stack + stackSize, stackSize, + 16, OS_THREAD_ATTRIB_AFFINITY_ANY); + + *handle = thread; + // TODO revisit this + OSSetThreadRunQuantum(thread, 1000); // force yield after 1 millisecond + OSResumeThread(thread); +} + +void Thread_Detach(void* handle) { + OSDetachThread((OSThread*)handle); +} + +void Thread_Join(void* handle) { + int result; + OSJoinThread((OSThread*)handle, &result); +} + +void* Mutex_Create(const char* name) { + OSFastMutex* mutex = (OSFastMutex*)Mem_Alloc(1, sizeof(OSFastMutex), "mutex"); + + OSFastMutex_Init(mutex, name); + return mutex; +} + +void Mutex_Free(void* handle) { + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + OSFastMutex_Lock((OSFastMutex*)handle); +} + +void Mutex_Unlock(void* handle) { + OSFastMutex_Unlock((OSFastMutex*)handle); +} + +void* Waitable_Create(const char* name) { + OSEvent* event = (OSEvent*)Mem_Alloc(1, sizeof(OSEvent), "waitable"); + + OSInitEvent(event, false, OS_EVENT_MODE_AUTO); + return event; +} + +void Waitable_Free(void* handle) { + OSResetEvent((OSEvent*)handle); + Mem_Free(handle); +} + +void Waitable_Signal(void* handle) { + OSSignalEvent((OSEvent*)handle); +} + +void Waitable_Wait(void* handle) { + OSWaitEvent((OSEvent*)handle); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + OSTime timeout = OSMillisecondsToTicks(milliseconds); + OSWaitEventWithTimeout((OSEvent*)handle, timeout); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +union SocketAddress { + struct sockaddr raw; + struct sockaddr_in v4; + struct sockaddr_storage total; +}; + +static cc_result ParseHost(const char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int res, i = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + res = getaddrinfo(host, portRaw, &hints, &result); + if (res == EAI_AGAIN) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + /* Prefer IPv4 addresses first */ + // TODO: Wii U has no ipv6 support anyways? + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family != AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family == AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + union SocketAddress* addr = (union SocketAddress*)addrs[0].data; + char str[NATIVE_STR_LEN]; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + if (inet_pton(AF_INET, str, &addr->v4.sin_addr) > 0) { + addr->v4.sin_family = AF_INET; + addr->v4.sin_port = htons(port); + + addrs[0].size = sizeof(addr->v4); + *numValidAddrs = 1; + return 0; + } + return ParseHost(str, port, addrs, numValidAddrs); +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + cc_result res; + + *s = socket(raw->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (*s == -1) return errno; + + if (nonblocking) { + int blocking_raw = -1; /* non-blocking mode */ + ioctl(*s, FIONBIO, &blocking_raw); + } + + res = connect(*s, raw, addr->size); + return res == -1 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = recv(s, data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return errno; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = send(s, data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + *modified = 0; return errno; +} + +void Socket_Close(cc_socket s) { + shutdown(s, SHUT_RDWR); + close(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + int flags; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + if (poll(&pfd, 1, 0) == -1) { *success = false; return errno; } + + /* to match select, closed socket still counts as readable */ + flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + char chars[NATIVE_STR_LEN]; + int len; + + /* For unrecognised error codes, strerror_r might return messages */ + /* such as 'No error information', which is not very useful */ + /* (could check errno here but quicker just to skip entirely) */ + if (res >= 1000) return false; + + len = strerror_r(res, chars, NATIVE_STR_LEN); + if (len == -1) return false; + + len = String_CalcLen(chars, NATIVE_STR_LEN); + String_AppendUtf8(dst, chars, len); + return true; +} + +void Platform_Init(void) { + WHBProcInit(); + mkdir("ClassiCube", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { return ERR_NOT_SUPPORTED; } +#endif diff --git a/src/Platform_Windows.c b/src/Platform_Windows.c new file mode 100644 index 0000000..7e66dbf --- /dev/null +++ b/src/Platform_Windows.c @@ -0,0 +1,1135 @@ +#include "Core.h" +#if defined CC_BUILD_WIN + +#include "_PlatformBase.h" +#include "Stream.h" +#include "SystemFonts.h" +#include "Funcs.h" +#include "Utils.h" +#include "Errors.h" + +#define WIN32_LEAN_AND_MEAN +#define NOSERVICE +#define NOMCX +#define NOIME +#ifndef UNICODE +#define UNICODE +#define _UNICODE +#endif +#include +#include + +/* === BEGIN shellapi.h === */ +#define SHELLAPI DECLSPEC_IMPORT +SHELLAPI HINSTANCE WINAPI ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, LPCWSTR parameters, LPCWSTR directory, INT showCmd); +SHELLAPI HINSTANCE WINAPI ShellExecuteA(HWND hwnd, LPCSTR operation, LPCSTR file, LPCSTR parameters, LPCSTR directory, INT showCmd); +/* === END shellapi.h === */ +/* === BEGIN wincrypt.h === */ +typedef struct _CRYPTOAPI_BLOB { + DWORD cbData; + BYTE* pbData; +} DATA_BLOB; + +static BOOL (WINAPI *_CryptProtectData )(DATA_BLOB* dataIn, PCWSTR dataDescr, PVOID entropy, PVOID reserved, PVOID promptStruct, DWORD flags, DATA_BLOB* dataOut); +static BOOL (WINAPI *_CryptUnprotectData)(DATA_BLOB* dataIn, PWSTR* dataDescr, PVOID entropy, PVOID reserved, PVOID promptStruct, DWORD flags, DATA_BLOB* dataOut); +/* === END wincrypt.h === */ + +static HANDLE heap; +const cc_result ReturnCode_FileShareViolation = ERROR_SHARING_VIOLATION; +const cc_result ReturnCode_FileNotFound = ERROR_FILE_NOT_FOUND; +const cc_result ReturnCode_SocketInProgess = WSAEINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = WSAEWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = ERROR_ALREADY_EXISTS; +const char* Platform_AppNameSuffix = ""; +cc_bool Platform_SingleProcess; + +/*########################################################################################################################* +*---------------------------------------------------------Memory----------------------------------------------------------* +*#########################################################################################################################*/ +#ifdef CC_BUILD_NOSTDLIB +void* Mem_Set(void* dst, cc_uint8 value, unsigned numBytes) { + char* dp = (char*)dst; + + while (numBytes--) *dp++ = value; /* TODO optimise */ + return dst; +} + +void* Mem_Copy(void* dst, const void* src, unsigned numBytes) { + char* sp = (char*)src; + char* dp = (char*)dst; + + while (numBytes--) *dp++ = *sp++; /* TODO optimise */ + return dst; +} + +void* Mem_Move(void* dst, const void* src, unsigned numBytes) { + char* sp = (char*)src; + char* dp = (char*)dst; + + /* Check if destination range overlaps source range */ + /* If this happens, then need to copy backwards */ + if (dp >= sp && dp < (sp + numBytes)) { + sp += numBytes; + dp += numBytes; + + while (numBytes--) *--dp = *--sp; + } else { + while (numBytes--) *dp++ = *sp++; + } + return dst; +} +#else +void* Mem_Set(void* dst, cc_uint8 value, unsigned numBytes) { return memset( dst, value, numBytes); } +void* Mem_Copy(void* dst, const void* src, unsigned numBytes) { return memcpy( dst, src, numBytes); } +void* Mem_Move(void* dst, const void* src, unsigned numBytes) { return memmove(dst, src, numBytes); } +#endif + +void* Mem_TryAlloc(cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? HeapAlloc(heap, 0, size) : NULL; +} + +void* Mem_TryAllocCleared(cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? HeapAlloc(heap, HEAP_ZERO_MEMORY, size) : NULL; +} + +void* Mem_TryRealloc(void* mem, cc_uint32 numElems, cc_uint32 elemsSize) { + cc_uint32 size = CalcMemSize(numElems, elemsSize); + return size ? HeapReAlloc(heap, 0, mem, size) : NULL; +} + +void Mem_Free(void* mem) { + if (mem) HeapFree(heap, 0, mem); +} + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +/* TODO: check this is actually accurate */ +static cc_uint64 sw_freqMul = 1, sw_freqDiv = 1; +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return ((end - beg) * sw_freqMul) / sw_freqDiv; +} + +static HANDLE conHandle; +static BOOL hasDebugger; + +void Platform_Log(const char* msg, int len) { + char tmp[2048 + 1]; + DWORD wrote; + + if (conHandle) { + WriteFile(conHandle, msg, len, &wrote, NULL); + WriteFile(conHandle, "\n", 1, &wrote, NULL); + } + + if (!hasDebugger) return; + len = min(len, 2048); + Mem_Copy(tmp, msg, len); tmp[len] = '\0'; + + OutputDebugStringA(tmp); + OutputDebugStringA("\n"); +} + +#define FILETIME_EPOCH 50491123200ULL +#define FILETIME_UNIX_EPOCH 11644473600ULL +#define FileTime_TotalSecs(time) ((time / 10000000) + FILETIME_EPOCH) +#define FileTime_UnixTime(time) ((time / 10000000) - FILETIME_UNIX_EPOCH) +TimeMS DateTime_CurrentUTC(void) { + FILETIME ft; + cc_uint64 raw; + + GetSystemTimeAsFileTime(&ft); + /* in 100 nanosecond units, since Jan 1 1601 */ + raw = ft.dwLowDateTime | ((cc_uint64)ft.dwHighDateTime << 32); + return FileTime_TotalSecs(raw); +} + +void DateTime_CurrentLocal(struct DateTime* t) { + SYSTEMTIME localTime; + GetLocalTime(&localTime); + + t->year = localTime.wYear; + t->month = localTime.wMonth; + t->day = localTime.wDay; + t->hour = localTime.wHour; + t->minute = localTime.wMinute; + t->second = localTime.wSecond; +} + +static cc_bool sw_highRes; +cc_uint64 Stopwatch_Measure(void) { + LARGE_INTEGER t; + FILETIME ft; + + if (sw_highRes) { + QueryPerformanceCounter(&t); + return (cc_uint64)t.QuadPart; + } else { + GetSystemTimeAsFileTime(&ft); + return (cc_uint64)ft.dwLowDateTime | ((cc_uint64)ft.dwHighDateTime << 32); + } +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +void Directory_GetCachePath(cc_string* path) { } + +cc_result Directory_Create(const cc_string* path) { + cc_winstring str; + cc_result res; + + Platform_EncodeString(&str, path); + if (CreateDirectoryW(str.uni, NULL)) return 0; + /* Windows 9x does not support W API functions */ + if ((res = GetLastError()) != ERROR_CALL_NOT_IMPLEMENTED) return res; + + return CreateDirectoryA(str.ansi, NULL) ? 0 : GetLastError(); +} + +int File_Exists(const cc_string* path) { + cc_winstring str; + DWORD attribs; + + Platform_EncodeString(&str, path); + attribs = GetFileAttributesW(str.uni); + + return attribs != INVALID_FILE_ATTRIBUTES && !(attribs & FILE_ATTRIBUTE_DIRECTORY); +} + +static cc_result Directory_EnumCore(const cc_string* dirPath, const cc_string* file, DWORD attribs, + void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[MAX_PATH + 10]; + /* ignore . and .. entry */ + if (file->length == 1 && file->buffer[0] == '.') return 0; + if (file->length == 2 && file->buffer[0] == '.' && file->buffer[1] == '.') return 0; + + String_InitArray(path, pathBuffer); + String_Format2(&path, "%s/%s", dirPath, file); + + if (attribs & FILE_ATTRIBUTE_DIRECTORY) return Directory_Enum(&path, obj, callback); + callback(&path, obj); + return 0; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[MAX_PATH + 10]; + WIN32_FIND_DATAW eW; + WIN32_FIND_DATAA eA; + int i, ansi = false; + cc_winstring str; + HANDLE find; + cc_result res; + + /* Need to append \* to search for files in directory */ + String_InitArray(path, pathBuffer); + String_Format1(&path, "%s\\*", dirPath); + Platform_EncodeString(&str, &path); + + find = FindFirstFileW(str.uni, &eW); + if (!find || find == INVALID_HANDLE_VALUE) { + if ((res = GetLastError()) != ERROR_CALL_NOT_IMPLEMENTED) return res; + ansi = true; + + /* Windows 9x does not support W API functions */ + find = FindFirstFileA(str.ansi, &eA); + if (find == INVALID_HANDLE_VALUE) return GetLastError(); + } + + if (ansi) { + do { + path.length = 0; + for (i = 0; i < MAX_PATH && eA.cFileName[i]; i++) { + String_Append(&path, Convert_CodepointToCP437(eA.cFileName[i])); + } + if ((res = Directory_EnumCore(dirPath, &path, eA.dwFileAttributes, obj, callback))) return res; + } while (FindNextFileA(find, &eA)); + } else { + do { + path.length = 0; + for (i = 0; i < MAX_PATH && eW.cFileName[i]; i++) { + /* TODO: UTF16 to codepoint conversion */ + String_Append(&path, Convert_CodepointToCP437(eW.cFileName[i])); + } + if ((res = Directory_EnumCore(dirPath, &path, eW.dwFileAttributes, obj, callback))) return res; + } while (FindNextFileW(find, &eW)); + } + + res = GetLastError(); /* return code from FindNextFile */ + FindClose(find); + return res == ERROR_NO_MORE_FILES ? 0 : res; +} + +static cc_result DoFileRaw(cc_file* file, const cc_winstring* str, DWORD access, DWORD createMode) { + cc_result res; + + *file = CreateFileW(str->uni, access, FILE_SHARE_READ, NULL, createMode, 0, NULL); + if (*file && *file != INVALID_HANDLE_VALUE) return 0; + if ((res = GetLastError()) != ERROR_CALL_NOT_IMPLEMENTED) return res; + + /* Windows 9x does not support W API functions */ + *file = CreateFileA(str->ansi, access, FILE_SHARE_READ, NULL, createMode, 0, NULL); + return *file != INVALID_HANDLE_VALUE ? 0 : GetLastError(); +} + +static cc_result DoFile(cc_file* file, const cc_string* path, DWORD access, DWORD createMode) { + cc_winstring str; + Platform_EncodeString(&str, path); + return DoFileRaw(file, &str, access, createMode); +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + return DoFile(file, path, GENERIC_READ, OPEN_EXISTING); +} +cc_result File_Create(cc_file* file, const cc_string* path) { + return DoFile(file, path, GENERIC_WRITE | GENERIC_READ, CREATE_ALWAYS); +} +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + return DoFile(file, path, GENERIC_WRITE | GENERIC_READ, OPEN_ALWAYS); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + BOOL success = ReadFile(file, data, count, bytesRead, NULL); + return success ? 0 : GetLastError(); +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + BOOL success = WriteFile(file, data, count, bytesWrote, NULL); + return success ? 0 : GetLastError(); +} + +cc_result File_Close(cc_file file) { + return CloseHandle(file) ? 0 : GetLastError(); +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[] = { FILE_BEGIN, FILE_CURRENT, FILE_END }; + DWORD pos = SetFilePointer(file, offset, NULL, modes[seekType]); + return pos != INVALID_SET_FILE_POINTER ? 0 : GetLastError(); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = SetFilePointer(file, 0, NULL, FILE_CURRENT); + return *pos != INVALID_SET_FILE_POINTER ? 0 : GetLastError(); +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + *len = GetFileSize(file, NULL); + return *len != INVALID_FILE_SIZE ? 0 : GetLastError(); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*#############################################################################################################p############*/ +void Thread_Sleep(cc_uint32 milliseconds) { Sleep(milliseconds); } +static DWORD WINAPI ExecThread(void* param) { + Thread_StartFunc func = (Thread_StartFunc)param; + func(); + return 0; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + DWORD threadID; + HANDLE thread = CreateThread(NULL, 0, ExecThread, (void*)func, CREATE_SUSPENDED, &threadID); + if (!thread) Logger_Abort2(GetLastError(), "Creating thread"); + + *handle = thread; + ResumeThread(thread); +} + +void Thread_Detach(void* handle) { + if (!CloseHandle((HANDLE)handle)) { + Logger_Abort2(GetLastError(), "Freeing thread handle"); + } +} + +void Thread_Join(void* handle) { + WaitForSingleObject((HANDLE)handle, INFINITE); + Thread_Detach(handle); +} + +void* Mutex_Create(const char* name) { + CRITICAL_SECTION* ptr = (CRITICAL_SECTION*)Mem_Alloc(1, sizeof(CRITICAL_SECTION), "mutex"); + InitializeCriticalSection(ptr); + return ptr; +} + +void Mutex_Free(void* handle) { + DeleteCriticalSection((CRITICAL_SECTION*)handle); + Mem_Free(handle); +} +void Mutex_Lock(void* handle) { EnterCriticalSection((CRITICAL_SECTION*)handle); } +void Mutex_Unlock(void* handle) { LeaveCriticalSection((CRITICAL_SECTION*)handle); } + +void* Waitable_Create(const char* name) { + void* handle = CreateEventA(NULL, false, false, NULL); + if (!handle) { + Logger_Abort2(GetLastError(), "Creating waitable"); + } + return handle; +} + +void Waitable_Free(void* handle) { + if (!CloseHandle((HANDLE)handle)) { + Logger_Abort2(GetLastError(), "Freeing waitable"); + } +} + +void Waitable_Signal(void* handle) { SetEvent((HANDLE)handle); } +void Waitable_Wait(void* handle) { + WaitForSingleObject((HANDLE)handle, INFINITE); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + WaitForSingleObject((HANDLE)handle, milliseconds); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Font/Text--------------------------------------------------------* +*#########################################################################################################################*/ +static void FontDirCallback(const cc_string* path, void* obj) { + static const cc_string fonExt = String_FromConst(".fon"); + /* Completely skip windows .FON files */ + if (String_CaselessEnds(path, &fonExt)) return; + SysFonts_Register(path, NULL); +} + +void Platform_LoadSysFonts(void) { + int i; + char winFolder[FILENAME_SIZE]; + WCHAR winTmp[FILENAME_SIZE]; + UINT winLen; + + cc_string dirs[2]; + String_InitArray(dirs[0], winFolder); + dirs[1] = String_FromReadonly("C:/WINNT35/system"); + + /* System folder path may not be C:/Windows */ + winLen = GetWindowsDirectoryW(winTmp, FILENAME_SIZE); + if (winLen) { + String_AppendUtf16(&dirs[0], winTmp, winLen * 2); + } else { + String_AppendConst(&dirs[0], "C:/Windows"); + } + String_AppendConst(&dirs[0], "/fonts"); + + for (i = 0; i < Array_Elems(dirs); i++) { + Directory_Enum(&dirs[i], NULL, FontDirCallback); + } +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +/* Sanity check to ensure cc_sockaddr struct is large enough to contain all socket addresses supported by this platform */ +static char sockaddr_size_check[sizeof(SOCKADDR_STORAGE) < CC_SOCKETADDR_MAXSIZE ? 1 : -1]; + +static int (WSAAPI *_WSAStartup)(WORD versionRequested, LPWSADATA wsaData); +static int (WSAAPI *_WSACleanup)(void); +static int (WSAAPI *_WSAGetLastError)(void); +static int (WSAAPI *_WSAStringToAddressW)(LPWSTR addressString, INT addressFamily, LPVOID protocolInfo, LPVOID address, LPINT addressLength); + +static int (WSAAPI *_socket)(int af, int type, int protocol); +static int (WSAAPI *_closesocket)(SOCKET s); +static int (WSAAPI *_connect)(SOCKET s, const struct sockaddr* name, int namelen); +static int (WSAAPI *_shutdown)(SOCKET s, int how); + +static int (WSAAPI *_ioctlsocket)(SOCKET s, long cmd, u_long* argp); +static int (WSAAPI *_getsockopt)(SOCKET s, int level, int optname, char* optval, int* optlen); +static int (WSAAPI *_recv)(SOCKET s, char* buf, int len, int flags); +static int (WSAAPI *_send)(SOCKET s, const char FAR * buf, int len, int flags); +static int (WSAAPI *_select)(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout); + +static struct hostent* (WSAAPI *_gethostbyname)(const char* name); +static unsigned short (WSAAPI *_htons)(u_short hostshort); +static int (WSAAPI *_getaddrinfo )(PCSTR nodeName, PCSTR serviceName, const ADDRINFOA* hints, PADDRINFOA* result); +static void (WSAAPI* _freeaddrinfo)(PADDRINFOA addrInfo); + + + +static INT WSAAPI FallbackParseAddress(LPWSTR addressString, INT addressFamily, LPVOID protocolInfo, LPVOID address, LPINT addressLength) { + SOCKADDR_IN* addr4 = (SOCKADDR_IN*)address; + cc_uint8* addr = (cc_uint8*)&addr4->sin_addr; + cc_string ip, parts[4 + 1]; + cc_winstring* addrStr = (cc_winstring*)addressString; + + ip = String_FromReadonly(addrStr->ansi); + /* 4+1 in case user tries '1.1.1.1.1' */ + if (String_UNSAFE_Split(&ip, '.', parts, 4 + 1) != 4) + return ERR_INVALID_ARGUMENT; + + if (!Convert_ParseUInt8(&parts[0], &addr[0]) || !Convert_ParseUInt8(&parts[1], &addr[1]) || + !Convert_ParseUInt8(&parts[2], &addr[2]) || !Convert_ParseUInt8(&parts[3], &addr[3])) + return ERR_INVALID_ARGUMENT; + + addr4->sin_family = AF_INET; + return 0; +} + +static void LoadWinsockFuncs(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(WSAStartup), DynamicLib_Sym(WSACleanup), + DynamicLib_Sym(WSAGetLastError), DynamicLib_Sym(WSAStringToAddressW), + DynamicLib_Sym(socket), DynamicLib_Sym(closesocket), + DynamicLib_Sym(connect), DynamicLib_Sym(shutdown), + DynamicLib_Sym(ioctlsocket), DynamicLib_Sym(getsockopt), + DynamicLib_Sym(gethostbyname), DynamicLib_Sym(htons), + DynamicLib_Sym(getaddrinfo), DynamicLib_Sym(freeaddrinfo), + DynamicLib_Sym(recv), DynamicLib_Sym(send), DynamicLib_Sym(select) + }; + static const cc_string winsock1 = String_FromConst("wsock32.DLL"); + static const cc_string winsock2 = String_FromConst("WS2_32.DLL"); + void* lib; + + DynamicLib_LoadAll(&winsock2, funcs, Array_Elems(funcs), &lib); + /* Windows 95 is missing WS2_32 dll */ + if (!_WSAStartup) DynamicLib_LoadAll(&winsock1, funcs, Array_Elems(funcs), &lib); + + /* Fallback for older OS versions which lack WSAStringToAddressW */ + if (!_WSAStringToAddressW) _WSAStringToAddressW = FallbackParseAddress; +} + +static cc_result ParseHostOld(char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + struct hostent* res; + cc_result wsa_res; + SOCKADDR_IN* addr4; + char* src_addr; + int i; + + res = _gethostbyname(host); + + if (!res) { + wsa_res = _WSAGetLastError(); + + if (wsa_res == WSAHOST_NOT_FOUND) return SOCK_ERR_UNKNOWN_HOST; + return ERR_INVALID_ARGUMENT; + } + + /* per MSDN, should only be getting AF_INET returned from this */ + if (res->h_addrtype != AF_INET) return ERR_INVALID_ARGUMENT; + if (!res->h_addr_list) return ERR_INVALID_ARGUMENT; + + for (i = 0; i < SOCKET_MAX_ADDRS; i++) + { + src_addr = res->h_addr_list[i]; + if (!src_addr) break; + addrs[i].size = sizeof(SOCKADDR_IN); + + addr4 = (SOCKADDR_IN*)addrs[i].data; + addr4->sin_family = AF_INET; + addr4->sin_port = _htons(port); + addr4->sin_addr = *(IN_ADDR*)src_addr; + } + + *numValidAddrs = i; + /* Must have at least one IPv4 address */ + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +static cc_result ParseHostNew(char* host, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int res, i = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + res = _getaddrinfo(host, portRaw, &hints, &result); + if (res == EAI_NONAME) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + /* Prefer IPv4 addresses first */ + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family != AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next) + { + if (cur->ai_family == AF_INET) continue; + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); i++; + } + + _freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + SOCKADDR_IN* addr4 = (SOCKADDR_IN* )addrs[0].data; + SOCKADDR_IN6* addr6 = (SOCKADDR_IN6*)addrs[0].data; + cc_winstring str; + INT size; + + *numValidAddrs = 0; + Platform_EncodeString(&str, address); + + size = sizeof(*addr4); + if (!_WSAStringToAddressW(str.uni, AF_INET, NULL, addr4, &size)) { + addr4->sin_port = _htons(port); + + addrs[0].size = size; + *numValidAddrs = 1; + return 0; + } + + size = sizeof(*addr6); + if (!_WSAStringToAddressW(str.uni, AF_INET6, NULL, addr6, &size)) { + addr6->sin6_port = _htons(port); + + addrs[0].size = size; + *numValidAddrs = 1; + return 0; + } + + if (_getaddrinfo) { + return ParseHostNew(str.ansi, port, addrs, numValidAddrs); + } else { + return ParseHostOld(str.ansi, port, addrs, numValidAddrs); + } +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + SOCKADDR* raw_addr = (SOCKADDR*)addr->data; + cc_result res; + + *s = _socket(raw_addr->sa_family, SOCK_STREAM, IPPROTO_TCP); + if (*s == -1) return _WSAGetLastError(); + + if (nonblocking) { + u_long blockingMode = -1; /* non-blocking mode */ + _ioctlsocket(*s, FIONBIO, &blockingMode); + } + + res = _connect(*s, raw_addr, addr->size); + return res == -1 ? _WSAGetLastError() : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = _recv(s, (char*)data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return _WSAGetLastError(); +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = _send(s, (const char*)data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + *modified = 0; return _WSAGetLastError(); +} + +void Socket_Close(cc_socket s) { + _shutdown(s, SD_BOTH); + _closesocket(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + fd_set set; + struct timeval time = { 0 }; + int selectCount; + + set.fd_count = 1; + set.fd_array[0] = s; + + if (mode == SOCKET_POLL_READ) { + selectCount = _select(1, &set, NULL, NULL, &time); + } else { + selectCount = _select(1, NULL, &set, NULL, &time); + } + + if (selectCount == -1) { *success = false; return _WSAGetLastError(); } + + *success = set.fd_count != 0; return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + int resultSize = sizeof(cc_result); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + _getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Process/Module------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Process_OpenSupported = true; + +static cc_result Process_RawGetExePath(cc_winstring* path, int* len) { + cc_result res; + + /* If GetModuleFileNameA fails.. that's a serious problem */ + *len = GetModuleFileNameA(NULL, path->ansi, NATIVE_STR_LEN); + path->ansi[*len] = '\0'; + if (!(*len)) return GetLastError(); + + *len = GetModuleFileNameW(NULL, path->uni, NATIVE_STR_LEN); + path->uni[*len] = '\0'; + if (*len) return 0; + + /* GetModuleFileNameW can fail on Win 9x */ + res = GetLastError(); + if (res == ERROR_CALL_NOT_IMPLEMENTED) res = 0; + return res; +} + +cc_result Process_StartGame2(const cc_string* args, int numArgs) { + union STARTUPINFO_union { + STARTUPINFOW wide; + STARTUPINFOA ansi; + } si = { 0 }; // less compiler warnings this way + + cc_winstring path; + cc_string argv; char argvBuffer[NATIVE_STR_LEN]; + PROCESS_INFORMATION pi = { 0 }; + cc_winstring raw; + cc_result res; + int len, i; + + if (Platform_SingleProcess) return SetGameArgs(args, numArgs); + if ((res = Process_RawGetExePath(&path, &len))) return res; + si.wide.cb = sizeof(STARTUPINFOW); + + String_InitArray(argv, argvBuffer); + /* Game doesn't actually care about argv[0] */ + String_AppendConst(&argv, "cc"); + for (i = 0; i < numArgs; i++) + { + if (String_IndexOf(&args[i], ' ') >= 0) { + String_Format1(&argv, " \"%s\"", &args[i]); + } else { + String_Format1(&argv, " %s", &args[i]); + } + } + Platform_EncodeString(&raw, &argv); + + if (path.uni[0]) { + if (!CreateProcessW(path.uni, raw.uni, NULL, NULL, + false, 0, NULL, NULL, &si.wide, &pi)) return GetLastError(); + } else { + /* Windows 9x does not support W API functions */ + if (!CreateProcessA(path.ansi, raw.ansi, NULL, NULL, + false, 0, NULL, NULL, &si.ansi, &pi)) return GetLastError(); + } + + /* Don't leak memory for process return code */ + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return 0; +} + +void Process_Exit(cc_result code) { ExitProcess(code); } +cc_result Process_StartOpen(const cc_string* args) { + cc_winstring str; + cc_uintptr res; + Platform_EncodeString(&str, args); + + res = (cc_uintptr)ShellExecuteW(NULL, NULL, str.uni, NULL, NULL, SW_SHOWNORMAL); + /* Windows 9x always returns "error 0" for ShellExecuteW */ + if (res == 0) res = (cc_uintptr)ShellExecuteA(NULL, NULL, str.ansi, NULL, NULL, SW_SHOWNORMAL); + + /* MSDN: "If the function succeeds, it returns a value greater than 32. If the function fails, */ + /* it returns an error value that indicates the cause of the failure" */ + return res > 32 ? 0 : (cc_result)res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Updater----------------------------------------------------------* +*#########################################################################################################################*/ +#define UPDATE_TMP TEXT("CC_prev.exe") +#define UPDATE_SRC TEXT(UPDATE_FILE) +cc_bool Updater_Supported = true; + +#if defined _M_IX86 +const struct UpdaterInfo Updater_Info = { + "&eDirect3D 9 is recommended", 2, + { + { "Direct3D9", "ClassiCube.exe" }, + { "OpenGL", "ClassiCube.opengl.exe" } + } +}; +#elif defined _M_X64 +const struct UpdaterInfo Updater_Info = { + "&eDirect3D 9 is recommended", 2, + { + { "Direct3D9", "ClassiCube.64.exe" }, + { "OpenGL", "ClassiCube.64-opengl.exe" } + } +}; +#elif defined _M_ARM64 +const struct UpdaterInfo Updater_Info = { "", 1, { { "Direct3D11", "cc-arm64-d3d11.exe" } } }; +#elif defined _M_ARM +const struct UpdaterInfo Updater_Info = { "", 1, { { "Direct3D11", "cc-arm32-d3d11.exe" } } }; +#else +const struct UpdaterInfo Updater_Info = { "&eCompile latest source code to update", 0 }; +#endif + +cc_bool Updater_Clean(void) { + return DeleteFile(UPDATE_TMP) || GetLastError() == ERROR_FILE_NOT_FOUND; +} + +cc_result Updater_Start(const char** action) { + cc_winstring path; + cc_result res; + int len; + + *action = "Getting executable path"; + if ((res = Process_RawGetExePath(&path, &len))) return res; + + *action = "Moving executable to CC_prev.exe"; + if (!path.uni[0]) return ERR_NOT_SUPPORTED; /* MoveFileA returns ERROR_ACCESS_DENIED on Win 9x anyways */ + if (!MoveFileExW(path.uni, UPDATE_TMP, MOVEFILE_REPLACE_EXISTING)) return GetLastError(); + + *action = "Replacing executable"; + if (!MoveFileExW(UPDATE_SRC, path.uni, MOVEFILE_REPLACE_EXISTING)) return GetLastError(); + + *action = "Restarting game"; + return Process_StartGame2(NULL, 0); +} + +cc_result Updater_GetBuildTime(cc_uint64* timestamp) { + cc_winstring path; + cc_file file; + FILETIME ft; + cc_uint64 raw; + cc_result res; + int len; + + if ((res = Process_RawGetExePath(&path, &len))) return res; + if ((res = DoFileRaw(&file, &path, GENERIC_READ, OPEN_EXISTING))) return res; + + if (GetFileTime(file, NULL, NULL, &ft)) { + raw = ft.dwLowDateTime | ((cc_uint64)ft.dwHighDateTime << 32); + *timestamp = FileTime_UnixTime(raw); + } else { + res = GetLastError(); + } + + File_Close(file); + return res; +} + +/* Don't need special execute permission on windows */ +cc_result Updater_MarkExecutable(void) { return 0; } +cc_result Updater_SetNewBuildTime(cc_uint64 timestamp) { + static const cc_string path = String_FromConst(UPDATE_FILE); + cc_file file; + FILETIME ft; + cc_uint64 raw; + cc_result res = File_OpenOrCreate(&file, &path); + if (res) return res; + + raw = 10000000 * (timestamp + FILETIME_UNIX_EPOCH); + ft.dwLowDateTime = (cc_uint32)raw; + ft.dwHighDateTime = (cc_uint32)(raw >> 32); + + if (!SetFileTime(file, NULL, NULL, &ft)) res = GetLastError(); + File_Close(file); + return res; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Dynamic lib-------------------------------------------------------* +*#########################################################################################################################*/ +const cc_string DynamicLib_Ext = String_FromConst(".dll"); +static cc_result dynamicErr; +static cc_bool loadingPlugin; + +void* DynamicLib_Load2(const cc_string* path) { + static cc_string plugins_dir = String_FromConst("plugins/"); + cc_winstring str; + void* lib; + + Platform_EncodeString(&str, path); + loadingPlugin = String_CaselessStarts(path, &plugins_dir); + + if ((lib = LoadLibraryW(str.uni))) return lib; + dynamicErr = GetLastError(); + if (dynamicErr != ERROR_CALL_NOT_IMPLEMENTED) return NULL; + + /* Windows 9x only supports A variants */ + lib = LoadLibraryA(str.ansi); + if (!lib) dynamicErr = GetLastError(); + return lib; +} + +void* DynamicLib_Get2(void* lib, const char* name) { + void* addr = GetProcAddress((HMODULE)lib, name); + if (!addr) dynamicErr = GetLastError(); + return addr; +} + +cc_bool DynamicLib_DescribeError(cc_string* dst) { + cc_result res = dynamicErr; + dynamicErr = 0; /* Reset error (match posix behaviour) */ + + Platform_DescribeError(res, dst); + String_Format1(dst, " (error %e)", &res); + + /* Plugin may have been compiled to load symbols from ClassiCube.exe, */ + /* but the user might have renamed it to something else */ + if (res == ERROR_MOD_NOT_FOUND && loadingPlugin) { + String_AppendConst(dst, "\n Make sure the ClassiCube executable is named ClassiCube.exe"); + } + if (res == ERROR_PROC_NOT_FOUND && loadingPlugin) { + String_AppendConst(dst, "\n The plugin or your game may be outdated"); + } + + /* User might be trying to use 32 bit plugin with 64 bit executable, or vice versa */ + if (res == ERROR_BAD_EXE_FORMAT && loadingPlugin) { + if (sizeof(cc_uintptr) == 4) { + String_AppendConst(dst, "\n Try using a 32 bit version of the plugin instead"); + } else { + String_AppendConst(dst, "\n Try using a 64 bit version of the plugin instead"); + } + } + return true; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_EncodeString(cc_winstring* dst, const cc_string* src) { + cc_unichar* uni; + char* ansi; + int i; + if (src->length > FILENAME_SIZE) Logger_Abort("String too long to expand"); + + uni = dst->uni; + for (i = 0; i < src->length; i++) { + *uni++ = Convert_CP437ToUnicode(src->buffer[i]); + } + *uni = '\0'; + + ansi = dst->ansi; + for (i = 0; i < src->length; i++) { + *ansi++ = (char)dst->uni[i]; + } + *ansi = '\0'; +} + +static void Platform_InitStopwatch(void) { + LARGE_INTEGER freq; + sw_highRes = QueryPerformanceFrequency(&freq); + + if (sw_highRes) { + sw_freqMul = 1000 * 1000; + sw_freqDiv = freq.QuadPart; + } else { sw_freqDiv = 10; } +} + +static BOOL (WINAPI *_AttachConsole)(DWORD processId); +static BOOL (WINAPI *_IsDebuggerPresent)(void); + +static void LoadKernelFuncs(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(AttachConsole), DynamicLib_Sym(IsDebuggerPresent) + }; + + static const cc_string kernel32 = String_FromConst("KERNEL32.DLL"); + void* lib; + DynamicLib_LoadAll(&kernel32, funcs, Array_Elems(funcs), &lib); +} + +void Platform_Init(void) { + WSADATA wsaData; + cc_result res; + + Platform_InitStopwatch(); + heap = GetProcessHeap(); + + LoadWinsockFuncs(); + res = _WSAStartup(MAKEWORD(2, 2), &wsaData); + if (res) Logger_SysWarn(res, "starting WSA"); + + LoadKernelFuncs(); + if (_IsDebuggerPresent) hasDebugger = _IsDebuggerPresent(); + /* For when user runs from command prompt */ +#if CC_WIN_BACKEND != CC_WIN_BACKEND_TERMINAL + if (_AttachConsole) _AttachConsole(-1); /* ATTACH_PARENT_PROCESS */ +#endif + + conHandle = GetStdHandle(STD_OUTPUT_HANDLE); + if (conHandle == INVALID_HANDLE_VALUE) conHandle = NULL; +} + +void Platform_Free(void) { + _WSACleanup(); + HeapDestroy(heap); +} + +cc_bool Platform_DescribeErrorExt(cc_result res, cc_string* dst, void* lib) { + WCHAR chars[NATIVE_STR_LEN]; + DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + if (lib) flags |= FORMAT_MESSAGE_FROM_HMODULE; + + res = FormatMessageW(flags, lib, res, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + chars, NATIVE_STR_LEN, NULL); + if (!res) return false; + + String_AppendUtf16(dst, chars, res * 2); + return true; +} + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + return Platform_DescribeErrorExt(res, dst, NULL); +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ + +static void LoadCryptFuncs(void) { + static const struct DynamicLibSym funcs[] = { + DynamicLib_Sym(CryptProtectData), DynamicLib_Sym(CryptUnprotectData) + }; + + static const cc_string crypt32 = String_FromConst("CRYPT32.DLL"); + void* lib; + DynamicLib_LoadAll(&crypt32, funcs, Array_Elems(funcs), &lib); +} + +cc_result Platform_Encrypt(const void* data, int len, cc_string* dst) { + DATA_BLOB input, output; + int i; + input.cbData = len; input.pbData = (BYTE*)data; + + if (!_CryptProtectData) LoadCryptFuncs(); + if (!_CryptProtectData) return ERR_NOT_SUPPORTED; + if (!_CryptProtectData(&input, NULL, NULL, NULL, NULL, 0, &output)) return GetLastError(); + + for (i = 0; i < output.cbData; i++) { + String_Append(dst, output.pbData[i]); + } + LocalFree(output.pbData); + return 0; +} +cc_result Platform_Decrypt(const void* data, int len, cc_string* dst) { + DATA_BLOB input, output; + int i; + input.cbData = len; input.pbData = (BYTE*)data; + + if (!_CryptUnprotectData) LoadCryptFuncs(); + if (!_CryptUnprotectData) return ERR_NOT_SUPPORTED; + if (!_CryptUnprotectData(&input, NULL, NULL, NULL, NULL, 0, &output)) return GetLastError(); + + for (i = 0; i < output.cbData; i++) { + String_Append(dst, output.pbData[i]); + } + LocalFree(output.pbData); + return 0; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Configuration-------------------------------------------------------* +*#########################################################################################################################*/ +static cc_string Platform_NextArg(STRING_REF cc_string* args) { + cc_string arg; + int end; + + /* get rid of leading spaces before arg */ + while (args->length && args->buffer[0] == ' ') { + *args = String_UNSAFE_SubstringAt(args, 1); + } + + if (args->length && args->buffer[0] == '"') { + /* "xy za" is used for arg with spaces */ + *args = String_UNSAFE_SubstringAt(args, 1); + end = String_IndexOf(args, '"'); + } else { + end = String_IndexOf(args, ' '); + } + + if (end == -1) { + arg = *args; + args->length = 0; + } else { + arg = String_UNSAFE_Substring(args, 0, end); + *args = String_UNSAFE_SubstringAt(args, end + 1); + } + return arg; +} + +int Platform_GetCommandLineArgs(int argc, STRING_REF char** argv, cc_string* args) { + cc_string cmdArgs = String_FromReadonly(GetCommandLineA()); + int i; + Platform_NextArg(&cmdArgs); /* skip exe path */ + if (Platform_SingleProcess) return GetGameArgs(args); + + for (i = 0; i < GAME_MAX_CMDARGS; i++) + { + args[i] = Platform_NextArg(&cmdArgs); + + if (!args[i].length) break; + } + return i; +} + +/* Detects if the game is running in Windows directory */ +/* This happens when ClassiCube is launched directly from shell process */ +/* (e.g. via clicking a search result in Windows 10 start menu) */ +static cc_bool IsProblematicWorkingDirectory(void) { + cc_string curDir, winDir; + char curPath[2048] = { 0 }; + char winPath[2048] = { 0 }; + + GetCurrentDirectoryA(2048, curPath); + GetSystemDirectoryA(winPath, 2048); + + curDir = String_FromReadonly(curPath); + winDir = String_FromReadonly(winPath); + + if (String_Equals(&curDir, &winDir)) { + Platform_LogConst("Working directory is System32! Changing to executable directory.."); + return true; + } + return false; +} + +cc_result Platform_SetDefaultCurrentDirectory(int argc, char** argv) { + cc_winstring path; + int i, len; + cc_result res; + if (!IsProblematicWorkingDirectory()) return 0; + + res = Process_RawGetExePath(&path, &len); + if (res) return res; + if (!path.uni[0]) return ERR_NOT_SUPPORTED; + /* Not implemented on ANSI only systems due to laziness */ + + /* Get rid of filename at end of directory */ + for (i = len - 1; i >= 0; i--, len--) + { + if (path.uni[i] == '/' || path.uni[i] == '\\') break; + } + + path.uni[len] = '\0'; + return SetCurrentDirectoryW(path.uni) ? 0 : GetLastError(); +} +#endif diff --git a/src/Platform_Xbox.c b/src/Platform_Xbox.c new file mode 100644 index 0000000..75bec25 --- /dev/null +++ b/src/Platform_Xbox.c @@ -0,0 +1,453 @@ +#include "Core.h" +#if defined CC_BUILD_XBOX +#include "_PlatformBase.h" +#include "Stream.h" +#include "Funcs.h" +#include "Utils.h" +#include "Errors.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "_PlatformConsole.h" + +const cc_result ReturnCode_FileShareViolation = ERROR_SHARING_VIOLATION; +const cc_result ReturnCode_FileNotFound = ERROR_FILE_NOT_FOUND; +const cc_result ReturnCode_SocketInProgess = EINPROGRESS; +const cc_result ReturnCode_SocketWouldBlock = EWOULDBLOCK; +const cc_result ReturnCode_DirectoryExists = ERROR_ALREADY_EXISTS; +const char* Platform_AppNameSuffix = " XBox"; + + +/*########################################################################################################################* +*------------------------------------------------------Logging/Time-------------------------------------------------------* +*#########################################################################################################################*/ +void Platform_Log(const char* msg, int len) { + char tmp[2048 + 1]; + len = min(len, 2048); + Mem_Copy(tmp, msg, len); tmp[len] = '\0'; + + // log to on-screen display + debugPrint("%s\n", tmp); + // log to cxbx-reloaded console + OutputDebugStringA(tmp); +} + +#define FILETIME_EPOCH 50491123200ULL +#define FILETIME_UNIX_EPOCH 11644473600ULL +#define FileTime_TotalSecs(time) ((time / 10000000) + FILETIME_EPOCH) +#define FileTime_UnixTime(time) ((time / 10000000) - FILETIME_UNIX_EPOCH) +TimeMS DateTime_CurrentUTC(void) { + LARGE_INTEGER ft; + + KeQuerySystemTime(&ft); + /* in 100 nanosecond units, since Jan 1 1601 */ + return FileTime_TotalSecs(ft.QuadPart); +} + +void DateTime_CurrentLocal(struct DateTime* t) { + SYSTEMTIME localTime; + GetLocalTime(&localTime); + + t->year = localTime.wYear; + t->month = localTime.wMonth; + t->day = localTime.wDay; + t->hour = localTime.wHour; + t->minute = localTime.wMinute; + t->second = localTime.wSecond; +} + +/* TODO: check this is actually accurate */ +static cc_uint64 sw_freqDiv = 1; +cc_uint64 Stopwatch_ElapsedMicroseconds(cc_uint64 beg, cc_uint64 end) { + if (end < beg) return 0; + return ((end - beg) * 1000000ULL) / sw_freqDiv; +} + +cc_uint64 Stopwatch_Measure(void) { + return KeQueryPerformanceCounter(); +} + +static void Stopwatch_Init(void) { + ULONGLONG freq = KeQueryPerformanceFrequency(); + sw_freqDiv = freq; +} + + +/*########################################################################################################################* +*-----------------------------------------------------Directory/File------------------------------------------------------* +*#########################################################################################################################*/ +static cc_string root_path = String_FromConst("E:\\ClassiCube\\"); +static BOOL hdd_mounted; + +static void GetNativePath(char* str, const cc_string* src) { + Mem_Copy(str, root_path.buffer, root_path.length); + str += root_path.length; + + // XBox kernel doesn't seem to convert / + for (int i = 0; i < src->length; i++) + { + char c = (char)src->buffer[i]; + if (c == '/') c = '\\'; + *str++ = c; + } + *str = '\0'; +} + +cc_result Directory_Create(const cc_string* path) { + if (!hdd_mounted) return ERR_NOT_SUPPORTED; + + char str[NATIVE_STR_LEN]; + cc_result res; + + GetNativePath(str, path); + return CreateDirectoryA(str, NULL) ? 0 : GetLastError(); +} + +int File_Exists(const cc_string* path) { + if (!hdd_mounted) return 0; + + char str[NATIVE_STR_LEN]; + DWORD attribs; + + GetNativePath(str, path); + attribs = GetFileAttributesA(str); + + return attribs != INVALID_FILE_ATTRIBUTES && !(attribs & FILE_ATTRIBUTE_DIRECTORY); +} + +static cc_result Directory_EnumCore(const cc_string* dirPath, const cc_string* file, DWORD attribs, + void* obj, Directory_EnumCallback callback) { + cc_string path; char pathBuffer[MAX_PATH + 10]; + /* ignore . and .. entry */ + if (file->length == 1 && file->buffer[0] == '.') return 0; + if (file->length == 2 && file->buffer[0] == '.' && file->buffer[1] == '.') return 0; + + String_InitArray(path, pathBuffer); + String_Format2(&path, "%s/%s", dirPath, file); + + if (attribs & FILE_ATTRIBUTE_DIRECTORY) return Directory_Enum(&path, obj, callback); + callback(&path, obj); + return 0; +} + +cc_result Directory_Enum(const cc_string* dirPath, void* obj, Directory_EnumCallback callback) { + if (!hdd_mounted) return ERR_NOT_SUPPORTED; + + cc_string path; char pathBuffer[MAX_PATH + 10]; + WIN32_FIND_DATAA eA; + char str[NATIVE_STR_LEN]; + HANDLE find; + cc_result res; + + /* Need to append \* to search for files in directory */ + String_InitArray(path, pathBuffer); + String_Format1(&path, "%s\\*", dirPath); + GetNativePath(str, &path); + + find = FindFirstFileA(str, &eA); + if (find == INVALID_HANDLE_VALUE) return GetLastError(); + + do { + path.length = 0; + for (int i = 0; i < MAX_PATH && eA.cFileName[i]; i++) + { + String_Append(&path, Convert_CodepointToCP437(eA.cFileName[i])); + } + if ((res = Directory_EnumCore(dirPath, &path, eA.dwFileAttributes, obj, callback))) return res; + } while (FindNextFileA(find, &eA)); + + res = GetLastError(); /* return code from FindNextFile */ + NtClose(find); + return res == ERROR_NO_MORE_FILES ? 0 : res; +} + +static cc_result DoFile(cc_file* file, const cc_string* path, DWORD access, DWORD createMode) { + char str[NATIVE_STR_LEN]; + GetNativePath(str, path); + cc_result res; + + *file = CreateFileA(str, access, FILE_SHARE_READ, NULL, createMode, 0, NULL); + return *file != INVALID_HANDLE_VALUE ? 0 : GetLastError(); +} + +cc_result File_Open(cc_file* file, const cc_string* path) { + if (!hdd_mounted) return ReturnCode_FileNotFound; + return DoFile(file, path, GENERIC_READ, OPEN_EXISTING); +} + +cc_result File_Create(cc_file* file, const cc_string* path) { + if (!hdd_mounted) return ERR_NOT_SUPPORTED; + return DoFile(file, path, GENERIC_WRITE | GENERIC_READ, CREATE_ALWAYS); +} + +cc_result File_OpenOrCreate(cc_file* file, const cc_string* path) { + if (!hdd_mounted) return ERR_NOT_SUPPORTED; + return DoFile(file, path, GENERIC_WRITE | GENERIC_READ, OPEN_ALWAYS); +} + +cc_result File_Read(cc_file file, void* data, cc_uint32 count, cc_uint32* bytesRead) { + BOOL success = ReadFile(file, data, count, bytesRead, NULL); + return success ? 0 : GetLastError(); +} + +cc_result File_Write(cc_file file, const void* data, cc_uint32 count, cc_uint32* bytesWrote) { + BOOL success = WriteFile(file, data, count, bytesWrote, NULL); + return success ? 0 : GetLastError(); +} + +cc_result File_Close(cc_file file) { + NTSTATUS status = NtClose(file); + return NT_SUCCESS(status) ? 0 : status; +} + +cc_result File_Seek(cc_file file, int offset, int seekType) { + static cc_uint8 modes[3] = { FILE_BEGIN, FILE_CURRENT, FILE_END }; + DWORD pos = SetFilePointer(file, offset, NULL, modes[seekType]); + return pos != INVALID_SET_FILE_POINTER ? 0 : GetLastError(); +} + +cc_result File_Position(cc_file file, cc_uint32* pos) { + *pos = SetFilePointer(file, 0, NULL, FILE_CURRENT); + return *pos != INVALID_SET_FILE_POINTER ? 0 : GetLastError(); +} + +cc_result File_Length(cc_file file, cc_uint32* len) { + *len = GetFileSize(file, NULL); + return *len != INVALID_FILE_SIZE ? 0 : GetLastError(); +} + + +/*########################################################################################################################* +*--------------------------------------------------------Threading--------------------------------------------------------* +*##########################################################################################################################*/ +static void WaitForSignal(HANDLE handle, LARGE_INTEGER* duration) { + for (;;) + { + NTSTATUS status = NtWaitForSingleObjectEx((HANDLE)handle, UserMode, FALSE, duration); + if (status != STATUS_ALERTED) break; + } +} + +void Thread_Sleep(cc_uint32 milliseconds) { Sleep(milliseconds); } +static DWORD WINAPI ExecThread(void* param) { + Thread_StartFunc func = (Thread_StartFunc)param; + func(); + return 0; +} + +void Thread_Run(void** handle, Thread_StartFunc func, int stackSize, const char* name) { + DWORD threadID; + HANDLE thread = CreateThread(NULL, stackSize, ExecThread, (void*)func, CREATE_SUSPENDED, &threadID); + if (!thread) Logger_Abort2(GetLastError(), "Creating thread"); + + *handle = thread; + NtResumeThread(thread, NULL); +} + +void Thread_Detach(void* handle) { + NTSTATUS status = NtClose((HANDLE)handle); + if (!NT_SUCCESS(status)) Logger_Abort2(status, "Freeing thread handle"); +} + +void Thread_Join(void* handle) { + WaitForSignal((HANDLE)handle, NULL); + Thread_Detach(handle); +} + +void* Mutex_Create(const char* name) { + CRITICAL_SECTION* ptr = (CRITICAL_SECTION*)Mem_Alloc(1, sizeof(CRITICAL_SECTION), "mutex"); + RtlInitializeCriticalSection(ptr); + return ptr; +} + +void Mutex_Free(void* handle) { + RtlDeleteCriticalSection((CRITICAL_SECTION*)handle); + Mem_Free(handle); +} + +void Mutex_Lock(void* handle) { + RtlEnterCriticalSection((CRITICAL_SECTION*)handle); +} + +void Mutex_Unlock(void* handle) { + RtlLeaveCriticalSection((CRITICAL_SECTION*)handle); +} + +void* Waitable_Create(const char* name) { + HANDLE handle; + NTSTATUS status = NtCreateEvent(&handle, NULL, SynchronizationEvent, false); + + if (!NT_SUCCESS(status)) Logger_Abort2(status, "Creating waitable"); + return handle; +} + +void Waitable_Free(void* handle) { + NTSTATUS status = NtClose((HANDLE)handle); + if (!NT_SUCCESS(status)) Logger_Abort2(status, "Freeing waitable"); +} + +void Waitable_Signal(void* handle) { + NtSetEvent((HANDLE)handle, NULL); +} + +void Waitable_Wait(void* handle) { + WaitForSignal((HANDLE)handle, NULL); +} + +void Waitable_WaitFor(void* handle, cc_uint32 milliseconds) { + LARGE_INTEGER duration; + duration.QuadPart = ((LONGLONG)milliseconds) * -10000; // negative for relative timeout + + WaitForSignal((HANDLE)handle, &duration); +} + + +/*########################################################################################################################* +*---------------------------------------------------------Socket----------------------------------------------------------* +*#########################################################################################################################*/ +cc_result Socket_ParseAddress(const cc_string* address, int port, cc_sockaddr* addrs, int* numValidAddrs) { + char str[NATIVE_STR_LEN]; + char portRaw[32]; cc_string portStr; + struct addrinfo hints = { 0 }; + struct addrinfo* result; + struct addrinfo* cur; + int i = 0; + + String_EncodeUtf8(str, address); + *numValidAddrs = 0; + + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + String_InitArray(portStr, portRaw); + String_AppendInt(&portStr, port); + portRaw[portStr.length] = '\0'; + + int res = lwip_getaddrinfo(str, portRaw, &hints, &result); + if (res == EAI_FAIL) return SOCK_ERR_UNKNOWN_HOST; + if (res) return res; + + for (cur = result; cur && i < SOCKET_MAX_ADDRS; cur = cur->ai_next, i++) + { + SocketAddr_Set(&addrs[i], cur->ai_addr, cur->ai_addrlen); + } + + lwip_freeaddrinfo(result); + *numValidAddrs = i; + return i == 0 ? ERR_INVALID_ARGUMENT : 0; +} + +cc_result Socket_Connect(cc_socket* s, cc_sockaddr* addr, cc_bool nonblocking) { + struct sockaddr* raw = (struct sockaddr*)addr->data; + int res; + + *s = lwip_socket(raw->sa_family, SOCK_STREAM, 0); + if (*s == -1) return errno; + + if (nonblocking) { + int blocking_raw = -1; /* non-blocking mode */ + lwip_ioctl(*s, FIONBIO, &blocking_raw); + } + + res = lwip_connect(*s, raw, addr->size); + return res == -1 ? errno : 0; +} + +cc_result Socket_Read(cc_socket s, cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int recvCount = lwip_recv(s, data, count, 0); + if (recvCount != -1) { *modified = recvCount; return 0; } + *modified = 0; return errno; +} + +cc_result Socket_Write(cc_socket s, const cc_uint8* data, cc_uint32 count, cc_uint32* modified) { + int sentCount = lwip_send(s, data, count, 0); + if (sentCount != -1) { *modified = sentCount; return 0; } + *modified = 0; return errno; +} + +void Socket_Close(cc_socket s) { + lwip_shutdown(s, SHUT_RDWR); + lwip_close(s); +} + +static cc_result Socket_Poll(cc_socket s, int mode, cc_bool* success) { + struct pollfd pfd; + int flags; + + pfd.fd = s; + pfd.events = mode == SOCKET_POLL_READ ? POLLIN : POLLOUT; + if (lwip_poll(&pfd, 1, 0) == -1) { *success = false; return errno; } + + /* to match select, closed socket still counts as readable */ + flags = mode == SOCKET_POLL_READ ? (POLLIN | POLLHUP) : POLLOUT; + *success = (pfd.revents & flags) != 0; + return 0; +} + +cc_result Socket_CheckReadable(cc_socket s, cc_bool* readable) { + return Socket_Poll(s, SOCKET_POLL_READ, readable); +} + +cc_result Socket_CheckWritable(cc_socket s, cc_bool* writable) { + socklen_t resultSize = sizeof(socklen_t); + cc_result res = Socket_Poll(s, SOCKET_POLL_WRITE, writable); + if (res || *writable) return res; + + /* https://stackoverflow.com/questions/29479953/so-error-value-after-successful-socket-operation */ + lwip_getsockopt(s, SOL_SOCKET, SO_ERROR, &res, &resultSize); + return res; +} + + +/*########################################################################################################################* +*--------------------------------------------------------Platform---------------------------------------------------------* +*#########################################################################################################################*/ +static void InitHDD(void) { + if (nxIsDriveMounted('E')) { + hdd_mounted = true; + } else { + hdd_mounted = nxMountDrive('E', "\\Device\\Harddisk0\\Partition1\\"); + } + + if (!hdd_mounted) { + Platform_LogConst("Failed to mount E:/ from Data partition"); + return; + } + Directory_Create(&String_Empty); // create root ClassiCube folder +} + +void Platform_Init(void) { + InitHDD(); + Stopwatch_Init(); +#ifndef CC_BUILD_CXBX + nxNetInit(NULL); +#endif +} + +void Platform_Free(void) { +} + +cc_bool Platform_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +cc_bool Process_OpenSupported = false; +cc_result Process_StartOpen(const cc_string* args) { + return ERR_NOT_SUPPORTED; +} + + +/*########################################################################################################################* +*-------------------------------------------------------Encryption--------------------------------------------------------* +*#########################################################################################################################*/ +static cc_result GetMachineID(cc_uint32* key) { + return ERR_NOT_SUPPORTED; +} +#endif diff --git a/src/Platform_Xbox360.c b/src/Platform_Xbox360.c new file mode 100644 index 0000000..86d4839 --- /dev/null +++ b/src/Platform_Xbox360.c @@ -0,0 +1,295 @@ +#include "Core.h" +#if defined CC_BUILD_XBOX360 +#include "_PlatformBase.h" +#include "Stream.h" +#include "Funcs.h" +#include "Utils.h" +#include "Errors.h" + +#define LWIP_SOCKET 1 +#include +#include +#include +#include +#include +#include +#include +#include