From 53cd78031302a52cf5c80ecec578cff1467297d0 Mon Sep 17 00:00:00 2001 From: Vanessa Date: Mon, 13 Oct 2025 15:12:35 +0200 Subject: [PATCH] Initial commit --- .gitignore | 8 + CWSDPMI.EXE | Bin 0 -> 21325 bytes Makefile | 140 ++++ app.rc | 25 + assets/appicon.ico | Bin 0 -> 15086 bytes assets/bg.bmp | Bin 0 -> 64310 bytes assets/cow.bmp | Bin 0 -> 2390 bytes assets/cow.xcf | Bin 0 -> 6456 bytes assets/cow_m.bmp | Bin 0 -> 2390 bytes assets/flaunch.pcm | Bin 0 -> 4502 bytes assets/font1.bmp | Bin 0 -> 1086 bytes assets/font1_m.bmp | Bin 0 -> 1086 bytes assets/getup.rad | Bin 0 -> 8245 bytes assets/rain.rad | Bin 0 -> 10447 bytes assets/spiral.rad | Bin 0 -> 8307 bytes assets/witch.bmp | Bin 0 -> 1270 bytes assets/witch.xcf | Bin 0 -> 3531 bytes assets/witch_m.bmp | Bin 0 -> 1270 bytes audio/Audio.cpp | 35 + audio/Audio.h | 22 + audio/AudioPlayer.cpp | 120 ++++ audio/AudioPlayer.h | 64 ++ audio/Music.cpp | 34 + audio/Music.h | 23 + audio/rad20player.cpp | 1097 ++++++++++++++++++++++++++++ audio/rad20player.h | 173 +++++ converter.cpp | 53 ++ graphics/Bitmap.cpp | 78 ++ graphics/Bitmap.h | 66 ++ graphics/Font.h | 67 ++ graphics/Rect.h | 45 ++ graphics/Sprite.h | 68 ++ install.bat | 2 + main.cpp | 111 +++ scenes/GameScene.cpp | 281 ++++++++ scenes/GameScene.h | 37 + scenes/IntroScene.cpp | 11 + scenes/IntroScene.h | 20 + scenes/MainMenuScene.cpp | 11 + scenes/MainMenuScene.h | 20 + scenes/Scene.cpp | 19 + scenes/Scene.h | 42 ++ system/Keyboard.h | 5 + system/Opl.h | 10 + system/Pic.h | 12 + system/SoundBlaster.h | 37 + system/Timer.h | 49 ++ system/Video.h | 63 ++ system/dos/Keyboard.cpp | 429 +++++++++++ system/dos/Keyboard.h | 182 +++++ system/dos/Opl.cpp | 18 + system/dos/SoundBlaster.cpp | 47 ++ system/dos/Timer.cpp | 106 +++ system/dos/Video.cpp | 135 ++++ system/dos/init.cpp | 12 + system/init.h | 9 + system/sdl/AudioBackend.cpp | 52 ++ system/sdl/AudioBackend.h | 26 + system/sdl/Events.cpp | 25 + system/sdl/Events.h | 14 + system/sdl/Keyboard.cpp | 821 +++++++++++++++++++++ system/sdl/Keyboard.h | 178 +++++ system/sdl/Opl.cpp | 1348 +++++++++++++++++++++++++++++++++++ system/sdl/SoundBlaster.cpp | 19 + system/sdl/Timer.cpp | 55 ++ system/sdl/Video.cpp | 125 ++++ system/sdl/init.cpp | 25 + util/Asm.h | 68 ++ util/Bmp.cpp | 97 +++ util/Bmp.h | 48 ++ util/Files.cpp | 27 + util/Files.h | 10 + util/Gbm.cpp | 68 ++ util/Gbm.h | 28 + util/Log.cpp | 22 + util/Log.h | 19 + 76 files changed, 6861 insertions(+) create mode 100644 .gitignore create mode 100644 CWSDPMI.EXE create mode 100644 Makefile create mode 100644 app.rc create mode 100644 assets/appicon.ico create mode 100644 assets/bg.bmp create mode 100644 assets/cow.bmp create mode 100644 assets/cow.xcf create mode 100644 assets/cow_m.bmp create mode 100644 assets/flaunch.pcm create mode 100644 assets/font1.bmp create mode 100644 assets/font1_m.bmp create mode 100644 assets/getup.rad create mode 100644 assets/rain.rad create mode 100644 assets/spiral.rad create mode 100644 assets/witch.bmp create mode 100644 assets/witch.xcf create mode 100644 assets/witch_m.bmp create mode 100644 audio/Audio.cpp create mode 100644 audio/Audio.h create mode 100644 audio/AudioPlayer.cpp create mode 100644 audio/AudioPlayer.h create mode 100644 audio/Music.cpp create mode 100644 audio/Music.h create mode 100644 audio/rad20player.cpp create mode 100644 audio/rad20player.h create mode 100644 converter.cpp create mode 100644 graphics/Bitmap.cpp create mode 100644 graphics/Bitmap.h create mode 100644 graphics/Font.h create mode 100644 graphics/Rect.h create mode 100644 graphics/Sprite.h create mode 100644 install.bat create mode 100644 main.cpp create mode 100644 scenes/GameScene.cpp create mode 100644 scenes/GameScene.h create mode 100644 scenes/IntroScene.cpp create mode 100644 scenes/IntroScene.h create mode 100644 scenes/MainMenuScene.cpp create mode 100644 scenes/MainMenuScene.h create mode 100644 scenes/Scene.cpp create mode 100644 scenes/Scene.h create mode 100644 system/Keyboard.h create mode 100644 system/Opl.h create mode 100644 system/Pic.h create mode 100644 system/SoundBlaster.h create mode 100644 system/Timer.h create mode 100644 system/Video.h create mode 100644 system/dos/Keyboard.cpp create mode 100644 system/dos/Keyboard.h create mode 100644 system/dos/Opl.cpp create mode 100644 system/dos/SoundBlaster.cpp create mode 100644 system/dos/Timer.cpp create mode 100644 system/dos/Video.cpp create mode 100644 system/dos/init.cpp create mode 100644 system/init.h create mode 100644 system/sdl/AudioBackend.cpp create mode 100644 system/sdl/AudioBackend.h create mode 100644 system/sdl/Events.cpp create mode 100644 system/sdl/Events.h create mode 100644 system/sdl/Keyboard.cpp create mode 100644 system/sdl/Keyboard.h create mode 100644 system/sdl/Opl.cpp create mode 100644 system/sdl/SoundBlaster.cpp create mode 100644 system/sdl/Timer.cpp create mode 100644 system/sdl/Video.cpp create mode 100644 system/sdl/init.cpp create mode 100644 util/Asm.h create mode 100644 util/Bmp.cpp create mode 100644 util/Bmp.h create mode 100644 util/Files.cpp create mode 100644 util/Files.h create mode 100644 util/Gbm.cpp create mode 100644 util/Gbm.h create mode 100644 util/Log.cpp create mode 100644 util/Log.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5626be --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea +release +debug +*.o +*.gbm +gbmconv +game.exe +game.img diff --git a/CWSDPMI.EXE b/CWSDPMI.EXE new file mode 100644 index 0000000000000000000000000000000000000000..68658929c4e5e71e5c157ed92cd3f00ad81d2780 GIT binary patch literal 21325 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 literal 0 HcmV?d00001 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..807581e --- /dev/null +++ b/Makefile @@ -0,0 +1,140 @@ +HOSTCXX:=$(CXX) + +ifeq ($(TARGET),SDL) +# SDL +CC=gcc +CXX=g++ +LD=g++ +STRIP=strip + +SDL_INCLUDE=$(shell pkg-config --cflags sdl2) +SDL_LIBS=$(shell pkg-config --libs sdl2) + +CFLAGS=-g -DBUILD_SDL $(SDL_INCLUDE) +CXXFLAGS=$(CFLAGS) +LDFLAGS= +LDLIBS=$(SDL_LIBS) -lstdc++ +EXE=game +EXTRA_FILES= +SYSTEM=SDL +PLATFORM_OBJ= +else ifeq ($(TARGET),SDLWIN) +# SDL on Windows +CC=i686-w64-mingw32-gcc +CXX=i686-w64-mingw32-g++ +LD=i686-w64-mingw32-g++ +STRIP=i686-w64-mingw32-strip +WINDRES=windres + +SDL_INCLUDE=-I../SDL2-2.32.10/i686-w64-mingw32/include/SDL2 +SDL_LDFLAGS=-L../SDL2-2.32.10/i686-w64-mingw32/lib +SDL_LIBS=-lmingw32 -lSDL2main -lSDL2.dll + +CFLAGS=-g -DBUILD_SDL $(SDL_INCLUDE) -m32 +CXXFLAGS=$(CFLAGS) +LDFLAGS=$(SDL_LDFLAGS) -mwindows +LDLIBS=$(SDL_LIBS) -lstdc++ +EXE=game.exe +EXTRA_FILES=../SDL2-2.32.10/i686-w64-mingw32/bin/SDL2.dll +EXTRA_FILES+=/usr/lib/gcc/i686-w64-mingw32/10-win32/libgcc_s_dw2-1.dll +EXTRA_FILES+=/usr/lib/gcc/i686-w64-mingw32/10-win32/libstdc++-6.dll +SYSTEM=SDL +PLATFORM_OBJ=app.res +else ifeq ($(TARGET),SDLWIN64) +# SDL on Windows +CC=x86_64-w64-mingw32-gcc +CXX=x86_64-w64-mingw32-g++ +LD=x86_64-w64-mingw32-g++ +STRIP=x86_64-w64-mingw32-strip +WINDRES=windres + +SDL_INCLUDE=-I../SDL2-2.32.10/x86_64-w64-mingw32/include/SDL2 +SDL_LDFLAGS=-L../SDL2-2.32.10/x86_64-w64-mingw32/lib +SDL_LIBS=-lmingw32 -lSDL2main -lSDL2.dll + +CFLAGS=-g -DBUILD_SDL $(SDL_INCLUDE) -m64 +CXXFLAGS=$(CFLAGS) +#LDFLAGS=$(SDL_LDFLAGS) -mwindows +LDFLAGS=$(SDL_LDFLAGS) +LDLIBS=$(SDL_LIBS) -lstdc++ +EXE=game.exe +EXTRA_FILES=../SDL2-2.32.10/x86_64-w64-mingw32/bin/SDL2.dll +EXTRA_FILES+=/mingw64/bin/libgcc_s_seh-1.dll +EXTRA_FILES+=/mingw64/bin/libstdc++-6.dll +SYSTEM=SDL +PLATFORM_OBJ=app.res +else +# DOS +CC=/usr/local/djgpp/bin/i586-pc-msdosdjgpp-gcc +CXX=/usr/local/djgpp/bin/i586-pc-msdosdjgpp-g++ +LD=/usr/local/djgpp/bin/i586-pc-msdosdjgpp-g++ +STRIP=i586-pc-msdosdjgpp-strip + +# FIXME: we override the host compiler to build tools here, maybe move tools to a separate Makefile +HOSTCXX=g++ + +CFLAGS=-O3 -march=i486 +CXXFLAGS=$(CFLAGS) +LDFLAGS= +LDLIBS= +EXE=game.exe +DPMI_HOST=CWSDPMI.EXE +EXTRA_FILES=install.bat $(DPMI_HOST) +SYSTEM=DOS +PLATFORM_OBJ= +endif + +OBJ=main.o +OBJ+=$(PLATFORM_OBJ) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard graphics/*.cpp)) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard audio/*.cpp)) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard util/*.cpp)) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard scenes/*.cpp)) + +ifeq ($(SYSTEM),SDL) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard system/sdl/*.cpp)) +else ifeq ($(SYSTEM),DOS) +OBJ+=$(patsubst %.cpp,%.o,$(wildcard system/dos/*.cpp)) +else +$(error Unknown SYSTEM variable:$(SYSTEM)) +endif + +CONVERTER_CXX=$(HOSTCXX) +CONVERTER_EXE=gbmconv +CONVERTER_SRC=converter.cpp +CONVERTER_SRC+=graphics/Bitmap.cpp +CONVERTER_SRC+=util/Files.cpp util/Bmp.cpp util/Gbm.cpp + +COMPILED_GFX_ASSETS=assets/font1.gbm assets/cow.gbm assets/witch.gbm +GFX_ASSETS=assets/bg.bmp $(COMPILED_GFX_ASSETS) +MUSIC_ASSETS=assets/rain.rad assets/getup.rad assets/spiral.rad + +RELEASE_FILES=$(EXE) $(GFX_ASSETS) $(MUSIC_ASSETS) $(EXTRA_FILES) + +FLOPPY_IMG=game.img + +.PHONY: all clean release debug assets floppy + +all: $(CONVERTER_EXE) $(EXE) + +clean: ; rm -rf $(OBJ) $(EXE) $(CONVERTER_EXE) $(FLOPPY_IMG) $(COMPILED_GFX_ASSETS) release debug + +release: all assets; $(STRIP) $(EXE); upx $(EXE); mkdir -p release; cp -Rv $(RELEASE_FILES) release/ + +debug: all assets; mkdir -p debug; cp -Rv $(RELEASE_FILES) debug/ + +floppy: release; dd if=/dev/zero of=$(FLOPPY_IMG) bs=512 count=2880 && mkfs.fat -F12 $(FLOPPY_IMG) && mcopy -i $(FLOPPY_IMG) -s release/* :: + +$(CONVERTER_EXE): $(CONVERTER_SRC); $(CONVERTER_CXX) -o $@ $(CONVERTER_SRC) + +$(EXE): $(OBJ); $(LD) $(LDFLAGS) -o $@ $(OBJ) $(LDLIBS) + +%.o : %.c ; $(CC) $(CFLAGS) -c $< -o $@ + +%.o : %.cpp ; $(CXX) $(CXXFLAGS) -c $< -o $@ + +assets: $(CONVERTER_EXE) $(GFX_ASSETS) + +%.gbm : %.bmp %_m.bmp $(CONVERTER_EXE) ; ./$(CONVERTER_EXE) $@ $^ + +%.res : %.rc ; $(WINDRES) $< -O coff -o $@ diff --git a/app.rc b/app.rc new file mode 100644 index 0000000..ce2f053 --- /dev/null +++ b/app.rc @@ -0,0 +1,25 @@ +1 VERSIONINFO +FILEVERSION 0,1,0,0 +PRODUCTVERSION 0,1,0,0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "neko-tools.de" + VALUE "FileDescription", "Just a game uwu" + VALUE "FileVersion", "0.1" + VALUE "InternalName", "game" + VALUE "LegalCopyright", "neko-tools.de" + VALUE "OriginalFilename", "game.exe" + VALUE "ProductName", "Game" + VALUE "ProductVersion", "0.1" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +1000 ICON "assets/appicon.ico" diff --git a/assets/appicon.ico b/assets/appicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..60ea1fd7a3fb45e9db470d30c9030a7bed9ddb1f GIT binary patch literal 15086 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 Y{KEaI1>bP{M3UM-{m>%e@>I$H1OCw9b^rhX literal 0 HcmV?d00001 diff --git a/assets/bg.bmp b/assets/bg.bmp new file mode 100644 index 0000000000000000000000000000000000000000..62fa6b4c819aa5568f23de7cadb4b58b8b2e3198 GIT binary patch literal 64310 zcmbTf-)n4NpXXJnN-C99D!-moDpi%_TlpqAN!9nHl9QbLI60L{kKNP$V5WIgkX9JM zcJxY6rx|fjhEbtWbi8pcW&{}!VH~`0T9Agh8q~p2^g{4TF9iPp!5##$z3}yZf7aT2 zS5D6JJncFsyY}ze>$N|>)@QA~>mT^j4}Vm1d048|E}8o^KK~w{OSM*P_nxP>HMH}dG}-O^x=ov{+AzWH}_v_zwvZkd$|9#+MoD(U;Bq&?rZ7V;Ewcq*Y|JmBlf9Kz= z{pO$fgWBhR?VqXr)Bnov)_&`E|D)P}{FnZP+F$&=->d!hU;58#f8pQzcWQtAFaI00 zzxJ2^t=d2GSN_e~pZu%8SNqrh{l8lK^Z)5zsr^@f^RLzZ^zZ+T+UtM$H*5dcfARaZ zKl|7JkJ=ynt^cI<`+w_i)c(W&{O{EMr~l?}*Z%AO_V3pIhyVWX*8YS4?tiWQ+kfwm zYJcv3{G-}m_@Dpp+8_LH|8MPo`9J@D?SKEnKdk-X|MmAFP^)Y5ZE085x7%!QHg_pI zjYhlO*5A8jFlZ0-*WshXTz}{@5BeSL-tYG({k=hdJYLUVxVpEuzpm0~H1_xR&VQtI z>h;$s_xFv)eYITuO6Bz1lD}{9vsvTcLfLKZw)oSA^8eyNX{oQ8B3!jbxtW7fVSS~T zUu$P~XSZ&CCBQ_o(>5TpV>0Xmuh0K}aS|_$$9sEw;|(Tzu>pE6tf(*lxEIvg;x~UZ zw?@5!?^l9%b@eU1(^G$4Q}t_Fo0c?P0?`hzD6Jj-5RujnQ)AGiC5G=73Bz)`FCi=W zRdyD!vUDtd@Y_lJI)Sm>?f@{*b&xSZ0^gwTxqg38oPxQf@j&2msns}N=J(@|=k*5A z063q|+ZCa!7xTX&z3<{{?m$TVc6OUPtsO26P&-JQiZJRQWsMEmWEm_furNcN`{-9a z&?SI6c~KVmwcC6{YiDN%THosKfSUm`@EM-?^#{X}U2)P{W(PKA>uBu$k5buluqCCH zxV0-nSMd84@x3E_5akl6Kx-H2q889HJ}61X{>*{Uos3%@4XTiCo#M;hDV9Q<_#9c~B4%>roo8;;kz6$9#%?NsmyMZu2) ztN4}{#Sea+wVmCB&+PggPJj$^8-y+R$?8QJum5as@q25#Z}1)OiQoKH0%v}u&~25o z62@PZUF??yusjNUFl$l)NgfMpI~Fl2T>&R}Div~`U0SS18{5>bCoMET9keTX{(f0& z?zBx0A>(k#nm?te$>NEd)YzpzSVzEc7PTGzac58(Y@G^DR^@!}$JaT|A=UB~j1_3@ zyk47c8zqWA{v0@KAXC|C9vlS2cd&g6PB>J(ik2?*+sl0^u4GRFD0a|8sQLRozvk}2 zPMfF%EyZiw&dGaIGO=(OtemiBGMLEG?oId;KAn75E97haalS9PyDG@cS<3uiw6(yg zv#|85E!>xyT+95-5cPepe5dZxFkEHUQ>xpb{-q1Zr!v2)23O99AEN%!R`gT+q^*wg zi1A97(H_OhF;p9M^HZR*A2&zzC?S1JkMHkKPp_6Uj8gu*rn8{4w|TE$?NbKfGvENs=Fvn9!kQaO+A&RZD_Cytre9pWXVtyWP@BzP#X zWRTLIAe3X5{$w(!*QLDg7whl2=v`g43nH)caJp$lA(&n+cPsXPcShubT{eQ!-hyAH zvX$Al%&NZ{6LtF_cu5Y;(kFWlQOP6VI|!9gYkGY>JKKzd-}IG zKHhA+&n~U~anJN%Pzd1FYYtleSi~wA10H%C|1v>ZIvBq1l`5=n=M|&W168Ae$2H1~ z&Y$IZrNJ%=lPFDD4dhS4w^QhA2A4jA-*{w8Hvrx~f0N1HF#@=^*K2F~w(Jw{71gfD zSzC=ZLtyJ4*XbsBNVV>a*|}A?zD`5xeT%26e~WBk4RKjPtD)I%1hC*|W)SPNcj6dq zT)VwP)N-(^utX+L#kMcahx2GpMx!wR*K_Lve0%K>z>T)(t*`693;rW%SRkwzx_0CA zlvpqk{1sJ}z2~oTK5Dkig>0YDTy9EcLd(+L@5o=l3Xrta3YytlNc?_D$^2;3P;e5t z$lAGCVQ3i`EnAFefb>+8_FytO=5pn2Rb=_v4MSUB?{zvt*9+gPdRao=5x}-&Q3yq_ zVi}8F-_?3|0j$U4jdpuTKB8NXc5H=oIssF_>Pt084pRB0}3o5& zXkIA10}SpaAQev5W-wX8ym2z-ADTO0g7Hfj+rIe{0GIv!UR%{Uy}!+-FDhLsYUfHU zPERibU}o}*K6QXfb)P!Hc;j`xzhAH5BV>LxUW!!KjFjdb&e?j+=JtBo4H=FLj-@Ox-?b!@ZTuL7dvXN7;K~rNH zf@N7Qw`*mb4CUp&mXS{$;w)1yk4;-@MHKE*JmAoEw!V|C{nF2#xpJPBW>sN15G)IGVcpPn0^5(e*3Vo1ZafkY{kkyvUUpI4?Y>8_e(L%_WiJFy%iaO25v$lq9~ zCajr^CX-R8A#meke`r>PJtTDPwqoY}UJw24>^D$iRZa{ug!Y41H0p)unbYkr@*w$G zYc2}~**tT4*f?#R%lA5rcn6=Ihs;^wMQG$39sm@S$xQW^nGEYJ2%J_*A(u6eAmSi& zVzmi5kg|nMD{?48RxK04WgG>iJ@yAP4T2Sau=WkVWr^bc{!y=;C2-wyJ=4-8Luh*j z^?k$PdjRusZ}%6w;^bS$KmLdxHNFEX*;4M$;aIQ?YBVNlMS4J{&y7^LfNntr#Y-!q z>D8baCzSR6+Rnk2u|r(`Len?0Hy%sQCdZSy*tyh$pXF{mIu7}h&8rlzlmw=7gb41p z_j?k-dV61~zdhw$GL1$SoyupYGN1SJXJvgkTd+I*+4=F0+&$>g`nK|}VYZYgu2?z< z2C=BWlOpXcDj#u&P@sm%0k9Rt6+r%M{{e+=lUIot(d6t>ePAhgERKhxG3r{XXFE`~ zk+I{+xa7BN3z#5o>NC^ZKVnY&_S*ip-zL|rxGj|{M=vFdG?()TdAI7PcPW{9j?i4? z{KvfxwwJUIId01v(xRkOsjIa*O<9K~D4f+5UNrAj@uF1&2e+JKhF!0B7gAIJG>-4^ zV_Q|`s@~Rz74nCX6+KyTeYz|mf0n*A)mDyk+0J&UW*4dqIyF~HC6|z|v?QvU72q_L z;|QE2+qG?nKhKos&}!lmiW;($;Ac#w0$XKEYzbJ1TwaJJH3^E+Lt}gQz35phxroGj z<}cUt}-B=C}QadA<~Uv?zN$0sk8t|ked zZC$TqR+$r(vKdk`Vizr>59SJCE7*aqBqo#hvtki^dyee_U@};ABuYd(jgTYBQh->n zU0%yJAw#@q2WzV1G-*eG7jhW)z{su9L@8tb_qm&LvU?+w3Yl6^&$Ix*UtS&_ zn0t88lFGwFwAsHUK?J$pY>=f_S%~f9|gc@eLFUB zUch;A*>hY4?RDx)BLE#tDvHg^3Yf7Ni6-=pg^6>(r=U;%S3(E#uzt*Mi8i03(PVF* zzrFgNjo$I3l)8lM6GalZtQktDBTTms4|lii_T9s+iq?Sh4Zsp*o--gO74zqINwn~? z#GU8z`MnFzhk#>K^NS(4su2>FYSlhySqD4 zySwdN>C@%$7Xi%N@@FT{@Tpc}^aGIs4xwF?wE za5zP#4-dG6{mK4bb{18oZ9fOOPoKc{^78o5?Oxp8wr}sc-H`zzfEizWw%bt5{-VO@ z&1^;dN`9xODf;1xj?aJWNzyhqN$l`xZ;_MS##g~s%#Im;GmsJ!Pf$xcMH*3R5JZI0 zi8y0y)e9nm$Q&@Sc!D$3Sb$sV=dj+Wbk6F2mZeHX6EbhQx=0iuendc9yI_JjdXYc?>v`C{}3ag%)ienp*-3LlE0pNSh5Fy_!`$Whvl zkp7j5_cJ^TUc|vE7S>f!#4DE+=)H??gGxzXXJ=Lq$*)isXwG_*KF?JK26` zGVX9Raw)i&$->ZaGGx%ML*hxaGUU5*d3t)%<;e)D{VzB1Y5zo_mDc#5q*EJCPL5A{ zC*2><)lfU<@8R(gc6W&&<}V?=>ui0SAbA7y4zG==)0x)KyO_L+9n->lC97{$0KtHv zateuA0SHBf5Z!|BunUl=x3IDUOvEadQXH&(7qUuOcQA^KzqLpP<4QdG$cOvi)a=o{gq zl^||vEs&YhWF{AxdN^)RNVQ3*pqSI;Y?IPCZyx)-RcMEH z^3Ey(s6TE)MF4R|*g#3(c%0ciF*CcF?MuY=8l+Z;pK&909p|0%<4;uWlQoPg_Rh~G zbCZYO{X-AM#gmLi-Aj=K)*B}*jCXI^u?x9A?V!_92*kT+G7ObSar2^f8tf842_ds| zQ02mf->9xicSzjyr!tWlLx`qRxdtK_ZJ^d5ML~r_dsPcyCul0GI8}(MrXgvi07?)M zZ5_$J0q~lO0N?RUcmBB15TYn7D_LlT-w+nDudKN6vfyNQ&mxqq64s6E$ONwM;8M)y?K|J(XL92W2>v zDaz)Es6uK;?K?7SvWr+j$1wsT!F*fPCJ}cIORhPcL15KtDT?Dpc05t-hoH)cu@Mo}msB*`GL7=R$?P)@bSzd%u#D2fXe5SGRB z?~pasiVfsU^mMQ{k(&UH$4|4TkUfcCv^0qJI>&0Oefo6KCS(@9;rQXP`!MQ0k0%c> ze7FbZ<;5*qN_rUw@YD`gtW9|(Vfrn)SaW+Dvg;%7ZplkT5XgvOrSR7L2tY}2x-M%} zskdr8>1W9^Ow;rno5tJCBK+^2Vf?yNqdO!#2e z7Cr!8v`;ZjqtWxH$sHQ@IC&gHXEYumdZW=0f3fH?3bjS>U?(nj--AbU?N;%4E*Al?m-6LQoSC81dNB%ur zT=l!%PtQ4e4gy(c)i`!y=2bTF7w`k1`Qf(j?k+AJc#4;^@EUNd93D?v>?JIaWA1uP z8c=DKqeK1AU$o_Y!4rz6A+diWwIBMORa)sN_r5oWk z_91t3^8~)7>Gn3BtpFG;wT?f$5{R@q?p|)kchcJl^!U8Im8H#+Q@ByFrIhPE_k968 z6*=byi??^!%t3QoWZ5u~QcHbepzs!B0l7 zXz((3d@Pp7XLoaaE!K`EFVo(b*tnpS1y7$Le6-!Fr2uH_#!^^0O$CooSp&F^kJU}q zLJZyRVFVL{ziTyk?(cm;m;mxTy?I7$rZ=J!B8Mz8H%2N$E<uGZ#k`uf zBnUP&4^*0GL)x`G>|=ANiI4@P0c}%>9DhQGkfQKGn~sw4Nb-m4#r!o}${kbyGW!WN zRskmQ?D2^J-aR6BpP=_JntZ)GJF5Z;fwXHZbl6$)hq{9cpZ|`~UX74KuzdTQ$_pYB=yt#gk20-G61j14Osi_n_2u}ys z#DU}j@YLg}+FyhA&6sw-m<~HkokBIc%+I+O37W~lSy3b_VdxnsPr4J!9{_7BPs&|U z77>OZLQnvi&gXaY(WkEnAF%H3dXqPv15%F)7A1gHw3`3cg(#7Fd%oGB&oP?6!gqXr zzD=Z_@a?SwP}Wd|^u`y!KJ$M%jWu5-MY$weTD0aGBn#2VpG?{}faL>ve74bV5+@S^ z#gURP7CHKB^@&fBldcqckKA(eDhuVH)8z&sfLK5@_;6+kR6AEPXAQRaF)0M`_+mc0 zpWi-kb006}x1p!sl2Us%%H7LcpcBiA1g6;@LlWB3VER%nF@I6GiDmmNudIAR&_$mD zd5v?){sk46lB*!6F~d}v-6n-9oxZ^&^3jK;O(SATf82D^qN#$JwDP)*CW%w&%Va9` zM@~k`)hHbzU;$JFGpirn9Pm7!ODTf}c)WMCN6Ul%l=GL_%N!;|e!bhffHa>k$l4=C zx{L@y4&P90{g$7sphk7b{FPtptH=>wx~S?$C6N`Yk29xhPIKLjSnyfasVI}_weT5S ze3+_P-%JOnbLTqvDRTpAMhxY=i$zb4n-r=6M;8bU2%_f%aCUHb*y3uCzoIH$WFiYC zf)p(MOy%WeIGWRO^eE?fC5{ObD}xt-A`q?1zW|`z$C19LEGbWr`BgG6TE)Jplsjk+ z8aG}uMa|czdpFOL$eTh0L0O@RuMt_~FUqyNH2jn~MY>2n(Cwxe#-f%i-jKmXY*k`M zNu56U063ETA%Wtzmaom9RLL@@%mMV$HU+qd;Oue!AdnH~X-=Y+gbi{+nn4!x7C<2@ zN`??lL|?IrxyvPQeVZrU+bH#0?66glXIIPCaYp~y$6l{ng=y;HvP*5w*cLzAQz>kl z+<*~c6g}=LEFvaR>`1gKOKpk7l`?+W5zNigHvA6PTFpKDAGIjNA?C#Kcu%1j!C9+y zehDoJ;meE2&1Mp|n3cfY%6(*(f>_R9{kVVsrtR%cR21H8b zAc)dq#V$UzvVdXx#LrHK6g(Et{GOh+!FLE6(<|{MjwGBdih3+|`{4NaGm4BL^8bOh z-^;`N<>FQmv*qs&Cu|EC^0zRR&wa$lB0mhuA{C}Y)%nl%dM77%^{$@GZPwPxag8jm zW}RK1YK{vQ&V4Z;-6U5qz9Cp4WvY;6EO*O#!duZ&1boAMfBbdwJbC`AV`zJU8C3kI z0gLgAuy4qTP|J>zFccsme*`L#l2J&c&o;d{nj@7-D+jhY=yor54Y+R!G)sNDuO4OlNa$^iO^9D4EKK2m@LxFozDJ#r_*H^ z#!2_2d+eclJn?6O>3NhC%lyo~)|cmrg$YAupDI|&8W58(3qIr}_>m|98lv}Aa;MOc zmNOLChyCDmhY9lMq;wa!7faVG91x^Orv zXoe(yyDl_3^ehhu=_k;CHRseHC#}y{_>3mYE8IjFZHS(P`~X1ASkN-nhZz4nLD0}* z7Rezp9k4^})@)7dQ=X*eWf=;81W;`dWefRV@@r2!a=v2{25PLD z+0>o}0or@)%0g{4S=sM`w)fJtrF*b1wkvtL4{gOJs?1SjDYq0|Zln-EaqRHd^Z)yO z-4n$95PUnk47hEzj`h+72`3^-5jjBSz9SkVa@wdR77^ilo?bIclb+?^CkZk-eOm0K z$Kq$m;%DiD^0ma4qpp?iq12k>1e7#W%gv_JnSJhn2GZc~X*TyIeWQ`}_vVIYwKfm8 zQ!CCl_}F%u*CO9xsl#Qnl1EP1hzBKi@OvYE3*AYugP*3f zhr18D7monsk%QKj?$Q3Xdg5d#iz1dGi6MnTi&AI0k{E^mowjC&+lSk6QJU`4 z-R4gwCJ3_w@a=9N+@8616Wc_nhMBb>4e@t@7K0A!DxM@;v>jRvgXZ8{GQWb@*8fjZva@ zebAaV0r-_a%%xh#)Vqn+HK*eyQ~oqnS>tINq>n#7WdKqPowBtM24Z=p&49x>CxRuwD!;8; zZbT|~cST|6-}|OR zM1%@}la&2potr8m=i*W2hIvb;-Yb;Smy_q0#%b6j{GwpOmTJ#StP;X6dIw7aC|#bk zuBZIR%=swUyXN#p!}OylZ7cZ;8I%G?(?$&O-|DQh@I?R(zU^%XfBQuXiW4-~b`Q3l z*OUa3L{LYtvrR0)Po2Vq!%8l6ZSe|55}?Q{hx4%Zrb!y3W{L@947aZytrOhds=3q= zI&*9<$Zl>08X-L;Jag+(XG8*p>c?`7Orf?*9Ixv&2`{*>m!=_!sPCG$ zK^kKA!dd)8&}QIC>3bY75g_;yzbt^phg+85(XHJbpP%>Cjw8F}@cCS`ZX2`q_V(sY z<+bR%{3WeBmHQH*YMw{vN)#2t2vQ+{`CM0+pCKC*^_L^v0$diokic~T6a%$bOwafv zeYNzgHRVS7E92Y)db#{OzWEAGc z9_z~sH&mcbi$>)lr}?i4UbDFA6J{FFp?}#5uJgD|0vKOselQV+P|4HN>CO%gda4d< zu?g=?$}q&x%A9xRkQTz|BB!kmPYIwWgfWVKZ2mlSXx7}GmGLpJ&Om&5>(IgB6^|t& za0nlT{_Sr>755QLT@##Af}&84N*0rY}hIh7RN-*Yb7eUA+3 zdXZ_+@RtOr1SRo`B7iEk_&kZL@9E_)w04pbY4QgYoCM%y14+dod+rblJOe_y>m^iI z$1k<6b&yKyW>Ui|0UVGx5eI>84u>}(_LKJVf4kY-euCdx3KaJEHtg-eA+0r>M~sI) zf4rR}N}`B)qpG(zQabqM=C0GJtY?oZJ0%9fH1F~*mY3HEab8~&z@P^|3g?q8eWWE* zJcX0ogg=7a!qgRB-@$;jddUikMfyfi0E&Gix;MMi9U3hFQ6%ku8c0f*he#rbe#@Bg zIgF{{k}aJ{@|1|tEb_-KumO|?!w>x}i_%%DM{(Vf20DK)$?6d$lPpkCZK>&<@+~PF zof4_qLh%c^T#v%B5hm%7D-*Qx>N$rz&{wS&;!CD-KMb9DeVzNB78kO9x+^()r|j(Z z^vGNFWnRI7De*9E1o4v+2S2w-Fl`8vS{lgYScA?dV_k4^!@BE~>m_346u?BkJ^R!C z(D(zD?d#?dy>QjCm`v5><4C2H7Z;am;zl#~1s%Px<|`Ib5J?0@8iOPb3A!2d~XMDPA z^F!JYB)Nv61aZpfL2TTlR%5N~f30S#kO0aRlZKH7$KVm-XSz^tCh9(VlI-2gN&)=# za=HAA%Y!-5Fl1gH9%bAgMN3i!I2NUMc`Tadc(BW;dvCYny)yMHlWBNFP#% zQoqc}_^DjZFBp}j9uDPOBvP541W$k3>2_PMKjRuy1-%OE@Bpb2!DRRZa4su2M;4ji zJH9xU7{l*ELRg?%uy?Uw9+{p*Poj9x1E9C)D=BNFBdD89U>qRsyHa83*$9RpDp8G$ zEg@=S#88FTv{5t{jb&$H`a>nZlsZ)QJ}L0c6!GKVq7@95lE%x+!^6~HsppL-b9hou zlJ(3uWu?5m^9HovRFEHmqE326hzheZex_R))OL%7E5{>9ZVPHfbdvEoxU1qr&Pov! zK)T%HMU(NJ`dZE5Nf}Qd7QDyx^pw_}q89}+atLswnZ{_i!MN@mVeF{umgyljWM_07 z1tx)@;X@?7X}{U}*#*yP#`iA4;HcD|O77h?ND1TR8AH@0k<8P%nRGAOY~Z3o62ZV{ z^?j6cxJ$?wLS#+M0SfHO24h~C@#Ww?RWE<=xb%WCl+5TGt+|?U3-@~5r-TW$4!{A(KdCUrk5?%sdE4)iAl*w@Zy+8i zCu#N>ROK_2v|M4)OOqYmC?|XvUKieNT4%=1-$9TmV&k9(Fv%c}xVB^gM7+v#6W3fL zaCjJ;HOT`2D|vwUDFCDkWVWjBDFD7PgIWHhj-aCl9kVAF>@sIv63w}_{7hY(QbU!; zhug!05}%zjxl|f-#n}A&N5$jkxLJ)Fk37W*Jxa$%3Zr)N8(hljFlW75SVL~DT0-Pa zPZPZfVXEA`I^ZUgE@R?S`z`b16B!#$x=SH|ESKkZGRu-m>Wa|_biEbIEBSLChp#~V z62wC6^31l}g#hwLZUwhNjQi3;V~sRe9>||upwx#4nw556Ts}yM&@a(&*BmBL{@D#< zWKimR%pjOHeQ}-h{nL|fpzpH(VI)+AAPbx=5(IN-QP#4G5MO2Cv*QOQMo<~6DS1zX zXDBw2h>ij1(3+4|MF9J>geh84kwDhiQVJgh(%JS&{GO7WHwz;uJms#QiSo4?HyO#sJbaWvdT+UJHi_60`8cw^1hX?F)VGZ^pBfJr&TDDlwaF~nC z4~zns9>q^?LHPj3D4A!4U)2cmh16;d%oH@hUZgIohnc%U69FhtMg=X{F_~VoL9wA0 zN&&oaE{1*ZAmYbkA`!WS@vF^ze>K>6d;8$PMlchUUv?2c|7W5$NAP>p@QoBs8(|(- z6ap!}6yB7A?>#)kWZElVoFT<|sB>nhp)iaVK0Q5=+#Ot=K<}Wdm#n<|9vm+AO{wDJ z`;rJM{6zf4oFX*pJ#wd{dNC^iW#Q=DKn!gRMKB^kLT~TKvFf{{M9-1v%^1DG7g!%O zT{}E1_(4wa0^o|Ij1DzHynC3pT8toSwTJ>0svuNmi(ND@xAT}wHuCb|K-!Bw%LF*H zVA((d%NuiM6jcQdZPr~{EVh)2pYjs4D26v?9Z~;exyfU;74}gW0 zHzK1qQ^UiT&bhUh5i03d@(b0Cz}m8-iChQ#ZXb|8080KYAB0cyl0&Cow3NgTZXyU+ zJPWQR=y)nE+ljOfQ49$wu0#=FE@}Yz+DuRcszu_JN(jKWsRbVlLLw+$5J?sxyfEHC zeHbL>kvJ!_0uC)XOBi~ZR&atP`D8fTM5;S@5Z-sTI|E%R`l2-l0Tq6sy5W?+;fMQe zt(o5i-DPT*iL98xm;dyDJn-K{Bidm)hhHula^w2dYJrgHx@4_vfH1e_U6xqAWY3X_ zz5VpnWU$8)bA0>a{*Af_#2?(A@JwB>+ORGasA|LY?ElVt+X@=&})=RR5cb}lp~c8LPqva@ylHLOOTTJu^DA` zg37q%YHlAGEhvU}&G}n;#3B_sbB?}D;<#DuhSesDPBQI;Md1YPni9{oXrFks4qJgqgqBYmd= zc)D|%`4#x!hfgq<$Gb&-kCH#i`~2KGzI?cwe}^9mg>t2Egb&R(TbD-)a!`b~{Pfcu zSe~`rHAf^$i)XW@gFS-qw{Q%zZB_tm4cUt`6V%TmS~AorK|z02t*_# zl91N+<)I=(h4Za&!K%KCFB6KpN(&q;0t789#f^_efa!Tyoxfg@Kth>PGqVWdoHgh` zW)f%`g-J;cgmL$S<{Bv%T7`>BoWGW}NPVF~iD51dbakkI_p@O9lsM1xts#Q?dc|#(B4oaFjoV0-a7|*uDIgX(1J%<6*^~h0PRRmc4pmAw zqLvB#G$=TnvmezCwt8whu9%;%(YN5~b4eG^C7m8zTr^c-Wwl&N`NI&lB!8@y|BzL* z{loi)|9GF@3;VCVLr{34sIBiyqq6$W3rV16w7J5smMoemIyq5+$1FR8Y{AJ;K>>6F zQE3EvLl|w9iuloL%Mpv@4UvQ&*?NkwS%I^jPe#tT@GWv>JH9<(Frpf)r?r202|r1U zDnM}y*JSRt5k2B(R+?WR)TUp^Z?4lJejHX2LY+E&t#uazhG~P0f+$^vgDLG%092S< zYHjNqg6f3>7&Xf$=95E~{F^Qz;hc;R+Qo?%yo02zQV3y;FQij@M(3|0>*BoFz%}Hr zSWYv>cZn5JbrZlve%s2S=bJnwk!A-y@8KD;{-ybFgezb$ag9xB^ zNC4F#ntiH3i}qrX=$%e>-tgl$|MV|?mrfS^G(|tjT`d4q2<fneh;Kd&F`}0_l8_r)OPq0Z|8eqXoeVb`HOWy1rMBW07QkO zFGzL9lgLVjOIXBKS>i-U#iVjxuc_%bkW$g$3<09(iFH3jT@gUJUmS=P*C5I6XX(5q z<*aK_Hus&PT>Iq(Sqr4|a7ClDR3gP9B+_3r*Zjyui{E0zlDOp$@gsf-2NEZHJ1zUl z)EqMi1TX_+@3_n?Jk6!GXslO|EzT04oi@knH>wt>^UHZE%ERE3JoXtUAu@fQ2642K zF!7kV=`vAsIiBM8HjT~1eyU+qJ$s&)^}DzLt}f|dt+j^L|0VpA|H#A^K5yZt_z(N{ z7F)R{f44kZg`Xlo3hG|$5HrM#rKB#R^AeO+nyut0E61MZU(O>6RNTo6AZ!OQY79b9 zF0ha?I@B{ieKP1JfhFXDUG96ES0xQeTSV_Az?d+RV$BZE{;#_7A-4S2SZR|0D`)P!d;k@3}O^k zGHkrSH|}HBrVLv}0)3KdQbWp%{FVGjsY(Rq46+Kj%KFRNN8pK{RP=jAsnz%SmG}w) ztMZFfYXVr*znrlb`K}&gyUB&_Ox{RGdAR0UwK2WwNdnbG!`>w^cw!s`E^pkEHOz&SCww46rOF6=+G_(5)UMh5G?a7?&`=QNI01NFJedsFkNtZ27QS5>i0-(wh5Z_ zEav6m1)WMXj*5%nH5ms|(j?u{CHrG@AkfV}JqSH6F14KD$&V+jn)Z@36yt#^{E$(vX9r>zF?3QNHKu4x)*$w zsNm5b0WFrAzm|j#S6>Bq;kW<2ZB}IU-OgMYTSd||2i&RhOWy~`gBn;tk1L?IxDvgS zgl@^DVJLJ3AG1!%Ku2%I0+6y;86X}c^K;HxX2B}Wcmy0t&>PXUneB84pYP-;!<6su zdG|O4bu~X6F!iVR&7d^N9E%l@7C*+4SN$(YbfC!aQK~Sn|C0viuMZI__-QOwQHnHx zlv*XOve;hEY<=Fhqm)k>0(IaEUv&VRsAJ3j&YKMyd2zEgRvzv?g z6&+^jtCI^+>{vp{7ElV95U9_gvO_WZgs|@HfDnz80{Z`82`=No5GbP0aRn`fblGJgGh`@ zep!DLM#0utR6W$DQL9Ok-hPwV0rhXJUNfRh|(zYtB-3#6kCia5eb$qM1f86EQqi|8g?97acu?}iU0II zz9fIV!G#yRSbvqzGeG#shfor~N`vk}ql+#V_*G@kC@cI9LxC6St|!S<7Uhp!!uS^w zxWuy^bi6Jeo0bqite{@pWVaye#UKCuPrjMo8*q#>@h=YmRmKfG0>s|g1d`DLAn)YR zN(_Q-14SoOf!^%fdnlR6{P;l`1{f&!2S3tJEp!ilp}>is0+w7z@>BCm(lpDPHMQ$xMpY+X7AA<8m~12O z^u$|xia5yFqa|oaEvQi9r$>eO$J3KZ{^lv4%&;`lsg!s5 zNpOAhxYL<7{-&oZ~t@x_pvb(jC{Km@1XG+?t%$C$cf8j`fIBmT?IgQ)h`v5@5j_k7qi;%~P$${}fmHOQSZ@U-Q!s zLg|64gF^;wh#qx65E*fMkKfAynH!G_e4n45@^BYh@GAtcDnvyd2I(xt54TWApa9nS zdz$iy+oZ3HuI}i0^);rU8)}mvNdl;9t6jmTq8t5OgEI9C{8$Knc^c%&zzCfuoW=Uf z6LCgp+rrJx_?q+JhyLnS><0&};3w@oU|6XD=5a$+`6Ic7ZS=76+>%JbwD6%jVfkaN zOxPO*E}Fq_%o~A8GJ?^Rqan;gzNeGZQ$4Z%mf)I3P$mS*C{yujNs0(Xd;-2VX|F%9 zikTgs`9Sy$BzsnBedw$d9%*2(=aU0|6p7M=pW;8sp!o5c7Ge`bPdcYzx#kyf3y*hL zjnhQIA&-nO3}1VFrvyK}NRR*Y-mCVN`qNFG4)m4^$C2_N9OtQLY+!NrHQtNh>(tfISeo-@ zfB4PK?{#MvkMYV5s+Rdle}mW}Ki&rT%lRdI@bjF+zKT!Rk-_V7-hj!GjAiij>BhU9 zVg(m=q&t0=r;s51Ib)C(gG_p?b_O4T5CMdtSr$JZptM#|yr(2j0eQ|P08+6H$Jxj- z!cy0*tI%US61(uf^Q+e@G+U2{6PO|}@WaMIDFQzp?+uWi@@O@m8on#x@~av%iu{?) zpENi~wgRBi7?H6rQ~5F4L|0Z6?-hEiR2JgJH8b^FC-Pp+9d-T6;XE~!*D6?Gw-6&j zI^+!)LZ=0V#v-bgp8kg#lap2nev(#h0b-f7R-zksfk+B}Ofr)zi9C-U)D=G-c?Kkx zj5NM-aXULWV!IFI&&gCqrdIh`EcMhJsU05A$7S!pP!ylr?9qZ z>XGPr0flT{x+mH$S-qM0!LC#1w+~{$(_Nd@EdwbfTsOx z*|PRf6zA9+?>4&+sGIM4DY zxp+yhc462!TB_{KZR(~oRs1gs?B<7BB68Q47bQIAdZUvkwel+X$mJs}@Z-t`IIm{6 zxpR~bf|k+7#KNU~v}809e8L7&FAnBEe~97E!8qhopm`%i9dXI;!(ty54xvMhQGOyu z6jLeD%9C=27iq9(S;b z3vBH7ZXs2{x!FV{D145l9}ztK2J-hh2O5q;p$zH8E`WB!;OWCjr#6Gz{S7 z0S2^po5!jzNLax!Nz>?He0t48nkpBvkhw@u0QK}FA*5FAcGsKU@{^~L?4{zvnoliV zzxXG)=ue{?eE_{-iypZMcB7uAfEV_steQ=me|;~+kg4KoWxm3Z_-HF|9P{oJEuPVy z(-#Yyiw1-90LYzw4Ea_H4O5DmXMRY5*hONy?M&+yEr~V?|KvXG}Bl9fx$PW_$fwZ6L zMTS6%>4z3%5OX2QvjK!L+gvPx41D8-2bcI*y%j+Y_<$d@qsM?=t8G=_=J4=|-nYxC z|BLaH#1jY#NmA6YyDJlqu=W@v(CqhHVIBkG7*=EY!PF+;FpCN^d`=d|05ar9FR7C~ zaIx@dJ}obBlpL}`F?MVx3{?8xzNK=|xPVxdMXgjfJ68Za7>U_8MeIy>agR*d(kP+T zQ#R^tX3T^59UMH--A_RRF@;Yz5f@IUNx?6k&TgK@w8e-*>{Q+Lv9ohDRR^KoVuiql z>IT4Kdd)qjm<|_j8I(9STP*1gs9t4_wd5m$XXdC=xR{?PTJ{Ql3a$wW-2;-2}*M88v{^rEE~4iY7dj(XH26^Jm;#qvi70&Ah{ z*LO(63s3%zq;Iv@#=cC1{xX7g#{Y+k`6otwQs z=UII&?ybXPE8LmE8fYK2XH$WnU%NKBo=h5z#+X0Kq#mD|KBd|G*=V>=N)Fb=nLp3j z=`u%X!n5W9kHuF3yJMZ6bf0NSP+}zmMcv@X$ zx;bXK_jK~p$w+$~O^@{d=o+ z3Q-1?WbO5JjYb`P=OAn~kJhMX^Q=YHe}d1(#wz6p-fPdo&57tm5k438``rCJ+w8(` z^LU-T_kS?W?#W2oeqt|9DSl_4&psc0u75t`o$BnqvB7W1yKvzZXsVAcde|UJ^M<@J zemdHRblUZLLA9M_M%8C?ebT7w*hYQ2%;H#bCIOoSz(6Uq9j=7m3DK`n2^s%2R~EBV zHeyWpa^%8{boP#W)!6QDIKO~p|&i2{S2mYNMol$l1JNapKb!8nH(|`D}0v0u|KRr-ejm=xK79WCdC1=xzl_lBZfqRa z>*cjtnj}=Jx_;`ZPAjWE5C9i`iGrW?ScR2-Jb`zLGXoR;BQg!7knLi?isebYzrIR? zyI&W?d}j9+3_vqJK>{zG?j zB=Xz=4I-g@{_y#O*nR%2-$H2f)6j$d)@Xm@xU1W(P8>cmKlZQ?aPI*y7`XOq_pfn} zS65SVJ&Kq-_hqe5M^r|IKD_K>Kty|tFzDoSK zo={SKmy7mO(tkWTnqYTe@R8sZ%jS*`#I5z(I&0n^fv&sw%^fI(EiM=uUT zrCPPsmDQhF-nyO|43d=r3f@mY&SvO7O^}o0^_3q`_mdOW&VJHx2>rfhw@Zj6C6P>V zk?{w=(MVTn1ri{xoS!u2SD#$Tz?fL_lSL>MKia##vbv|Anc*K}gT~2V#jj(=VeN*X z|G+DV5$RZ^6I%4bz-cx2s@_1D-|*uy#1xgV)t2S{fb8KF&fM{F^onk_=1qk%ho{Ke z{o2v#6>Ra1XGd^RIh&n1*}%KV)|%_$E2|kx zG9pf-5kfLf&{77j!*9YjmRH1Y#0~ognDb$FScIKm4#-m$GZCp(qCK@LR$K%EtBz+k9DZX}ZVS)sc?SQBcLR z=Aq15=EwZmk$ye!^z`(qdEXSj`z!5n_93`@n4QHa?K)bHL2Jo)36+Z2c)=4KgzyO& ze`kB^tK!FQto}(qkE0*B(j=kt)%M%Vc*j-qTdo7s8a*BjbT~;Ss^5U5>n@Qrd>BeH z_Ey&SRA()6PF!OOF|^L>b%gk>qdNYfEq;$iP&foI{2{r7EtH=%7&a7zCta2C=Vn9; zg@GTylPi@s(7zACZ>}{*Ylr6ddW9n~Kg%C#Yc#W&JO`!GumeGlkvGbw32Js06fI_k zXJI@;Y2e4svk$YM2xBIjn+(g*s3ubC`VH>DNc1u26Df{Hn^>k&1P0>A?VpT)SYKb| z=ZkXX^a%vY9{#Kl8s+K3OlngUv0_gAR@XTytyV4{X%jq^&UmZSCVtA_joFZSfPJl| zu}BmF1YDF4XP=cJebw>@g1qL{y?$H!>S%ssey`pQFo+>7Tz;z7a?q_srVtWslcj`p!5sDqQu$EkG;}iSvW(w3{d0ENofB(&5k$BitE(b|`mL|5Xin9$ z;3W>HLQXQf?Sep-OYrg5$x0|vW~Bg5>MKV_4&b%_L~aJS>gvieC<2@*vFDO(=U0cVpP%m8m+p-grBhjq}6-2h%e0ICLiY4PSU3~iJNuZ zQTJwZ#4Cfh6YPDqlgfHyh*@q8mO^M$e z1->ff&y0n8#%pNJZ_|ONMWwSjsCUK+Q-lx$Nuz?lBz4^k@H5xEve}(+jL;#0!En^= zO-)J(II7#}K!ZxL*XGDwwXB4Y5E8Vp@dx~Dbbk}xNN(GmWef=FO8`9_#BKpDe$rBS zFlIFQmSLs%nLR~K0l2;jVwkwPTWR`Ly$&s6fpF>MnILHA>0uD?vh+#*0;ER$>@)l0YzR($!N7r@0CtDn zQE!AW(yqb;3ZVQ4nCh&I7<#PRkL<*uAwD!r`gNLO%NAO;Yvao5O4`SwgxK*jp$K7n z*cl+cP+YDPQaXNatP@kFy``S~(-&Cni6|T9_W^Vk#EdXPUn3;f7`E>-GvYT#4xz*` z7(>J_$)EWJzBAs*(>!Vgzs;_`9kJ-tF&tyqRZDUk0w~F-VKGH+yl=&$}^L;H;o zDqwI#dw>RyHsxl6-yorP^Go{6S;-)vci1L185-@C)e>GX-Q(A7Hd8Y>ryCY1V zT|oMNB%Z8a6h|wXP3lXajQdwY!?GbVE*+9AmcPI!fXk>Z*%8&GL(2mTNC-Gek!!ob zZvv)csr?ED%}a2k_P7^Iw#3hK5lVWaCJ0!c^ooQ`NBux>S6}z>n&@{!kTf-XA?e!)kwWfptiOfm^xJR!&yf}7799;<)9^G2 zZ^5PL8lnXA6t)!mAiZoRTBf?S`sR|Lh+sj{U9Lf}wuBS@@FDUq<|jJ%V*8JhpYXZZ z{E1%z*p+*b#w-tH^u#b(K7A;Vv51ltR$b8@j-w839*GKmf(NCs_^G;`F$CDx)*XH~ zqtN-5W-grd}k#;i(knP^*y?u*U}G-aEP`ah1<{t!wdi>OI=7)H#PDm_CO8p&;jnUo)09RI(Q=*OnD7^fG z1g8Ux{3SoykxtF+A*No&Xv%XVcDx=zFO1Naav{D1{ILYjN67tV8yWm`a^h|(ty`F3 zfl%E)D#fZMIc95j;unUJ{ArfI!^AHi5%{dW@%0Yl2!7Ew5i5%U8ehpz_ud67Cvu^> zd^#5m$4Pr-Weh(_1(i^KXSA{Lyt2AVGL*H5g}0?!VOi=g<1G49h`4arGNvMI8gLmN zL_avfNM2Fvb>emhfs_0yDwq64!Le{FfUFY@+aw>^Sk!#2Ra**t1;5r&MgDLTVJ43X ze)n^^59AL?P3JLx@uMrkH~^?%u%7TK0VBav8V27X{bsu_n#*<#nG6QciJ!86kwrxg zv4S+fR>co^!EXhAe4k8}aw(U2FW3>DKuwIwA`RggdP6Q29EEPoh2*4OUUjhRZ~ zM*x#9xM3qmpQMs60WRj$1QuBVNJbKPkn=SdVf3sv9OoRwzWpt?T391~wvuRt zBoHx-bNLR+i?kV0D1d83GXTgenzbXWVq{^e{7U&-;Ky-m>OgnlLVUTJI1&u0Qalsa|2vuyWnu97hqvI_{QqDHxo!Qc!06cnl7H(d^lWc~^T=I(fv zuUE5lG%~NT?L@xFUyTe^l0QBW$%3C6aN(ZsN#El?zU9|cYBd)bz~lq*9o=CG4}z%f zXoy%w8>Mt0x*}bAC-|k_Bk>#dx^%_CZv*M0)~dXcs8-;OuLN>KTy3g$aXU;>mRP|~ zAdJY)0Qnv^Aj^!!Am)4KXo)XY)VV|!g^2b|e_UTC83^Nn_E%wJ1OUu-q`oTXG8WKn zkCrJ4VA9|B_&sL*rH8m^c_e^}zW6HD2xS;2!2?{BO|p80`bvMjC$%)c=P!~vUL=qe z7~{w9=L%nMQ`S$XUT$n<4Sr^z#F)sMAXgiGE`77=O3O~8R3wvH@7jAJ51~c=JTl{1 z&PU2gD<;mMO^v<=!0@4VA{hjKHOo4o3sP%%@lHp15Ekhpa#0dh06#?kx!IKRM~@O> zi2rEGgH)wY?X@t}x~BG|AN)wFx-dwC)JZrG``@&_;3s^J`ce$jo5=YJU$1BH;i@Tq zae015II!{5Fr6trR7LuAIs&K*s^F6eWuMI z*f&cb2#`Nwm}c`ZwSV~_8-NIYxOzlsL$nHhr?0P!K&BVCr4cc21<*H^@8@nJ|4rIv z0@jYCVBZN9J74=>-)}Wpxc^k^d4q%j>Z;(`V#or%L-Z0r@#Fv2HV%1pW#j7?!y}xd z`NauRQM|!2BM(H7GfQI3h1t3iR$3D#w_$)FERI*2z=fUa%j!C!;m178jGAql9jfjs zgfaj;Lyi(Yuap2r|1Ygw74y^cmjyo!!lD$;*X}PfMy#ZT{T#I_Yels4aQd}XuZ90$eh3{4M6TmY@y~JXyQ;$)! z-`?SObjo9`_laNN)zn_cU)=7JupJW+Xb7DFiWc{;-;p>5J_2Z6IHKAfJTv>uYKW0D z0OH1YJL5Ay#=V6G2fqM_{5?C!C6e>GbUsRB3!9~+V&)gC+l{X9#4hy}b!Y%-^*P!v zc^`A576u5c6>Nlf8lX;2;6kWhg@p$ukx;DBYi>*V3;hKbs+{Dn%CE$CM4I)(S==@T zGRhNcESS5@5Ib_ivfY`Vgi>{BEQ7jjeuy6mJJ~?`Hn4h=jmb?GKN``!lc+d!Tu|VZ z=dZb$khy3d`4=&%*QvS>iix887!0VLR@sA#TY;Z_O4^Yd?#t3P$t?GO99dae0v)2P zbk+Px0u`|o{Pe;>ht$#j6(QV2j$cl39H*yiM?Y!*l=m}4@HGM-onh9M!4Aq92~)U+Rd?O^`oZ_!xsTZbI4_L25Vf#G)|1x8jee~I7f#xwFK4ThE~ol2v< zy3Oi3c9Lff@>+R|etT6i$11?BC~mH=wpO^SWtu_wLi*}*FYJgETL87hfwq;4{PaUr z3;f`TMSTGwbYike@c55yt?Dn*M{918 z-OYq0bgRCh8@L@5g*o4ZI?a1=>&w7vvNuuBQOn2FBhR znkEH?p8#%b(BKVz)4*KePRfT&Bf`;@dn_m_`?Zlp*4*3B=gu+D{L9YpZwECTrKfX@p3r5nT%AWl~S zV3t3{(uv>ET67X?C}mNUbkGGPk4DBF1?kgT^3@E5Z*#Lpe-R0IE!YEVf2ud=KexLZ zYU_^B*Ju1acuO&N$;qo{Xvtr;HTf!(NH3a`K6w@%q&2;MC2D!EMQmT zLmK8MhMN;Sh{dn0zD5~{9~u_?xRxz8^Q~Jes62&OnSnrPf0y>%mLkb7bfwe04nRRk zc+pcGXMPw0x|dW93b-Y707G$w5OJ%O{Nz6}KV&XMtl7-`t{BWqQSa|;wwn3zq=5?c zLyeqwVjCHp>$kkosf%TN4B z)B;c!uDXCV@YTdA5&WsbPpg(Je#-xaj`A|^q`ySaM&+*GL&b^gssYd{)Im^OpJlG< z*^9a)u4zcxcyj~$L=iw%X)}dB1_1UxiXQ+M#gEWM|0IBf;VWk6$@8$K6aw*tqGf%9 zi|}prEqyQP{1bM0RZ48rHQEr1YUITT65;6bs#8MWzS$#`pz>USVZvkLYS? z&jmC_+)DODYpL`fHh(oMrn4=UKe-Sdp_ctq3k31|aK@0)*$TRRq;7OsKh~-O0Fp;* z&{;dAWg(uvCX(gw+uUGC0Rt$_X}{Gp43L;6LO?O$!sR`T zQ=*|1M`LAkMF}hgGPW&A{8$)tPoAWrQ2NLee*Pz@Md|h6*HhrPwJHVfZGL$keo+8z zdlC5bg~(o)<_U$!jSJEL9xKChGenC4vMar<%@z>HBCW3XEP%R9;j;rFQq=s4MBC%o zRFyXQ=UQq4OMNrG+yc({?0-}K}vj6!RK6|#jk+^V{(1HGPCIsMZr=H z$$Vnl;ive8#Rku^$QinfPM$NUk5J42v-Eiz{7yD)@>d10*ZcDIdGK8D!*$7A2w%9` zt<{P6flo0g_yXWSn_2ME^Q0|&3LaCT{i1>kL8>);i=wNApyZtME!7Z?&{v5fjW?Al zKD&=9KaU?z{1}*^!5cjtMDm&b&Qu!pvrJFK#?LY>QXv<<*m_fmo_$rZwZ2*KLx%w< z9bS+=5O%vKy-n4}{I+^u`_HI8Mdxs!u4;0VR<>7(R}e8pao$Mav-NS)$lzxI7QkK) z{SWy|*9l2Z&#jgKVptPDU21eR(EV1&;hCQr8b8riE3DrdK#mSm_~Id_qNF1EQ~v5o z-E*(69+JvxHH~wJ9bdrJ>yIYo)};T;g|HMCCK0gRj;Hv2Bg}&dj&(h^6+y=!k zQFIw75f?%0cMJK`jg$rQm9f$HU$L|@2V}k~9cdxezKwm-yAOaWwb>S|+V@}-E5YVk!A$i7NCsj-}B;z|s z+8VYGD;Py%T2z9K!oSREP2<<>KLF^su;5owU;*SiFb`XU$#aYE-VQON*M{qp!q9;c z-NRiHlj(}7`=G)Pf&y5?eHg)nvdcQ-BMcEhWnf&Wxa-EbrM}vR{C({4xE8Hk$-emY zunYmv+r6syl`bd?AQu?I%1P?x*Kgx}kv?$q1tbFXHM;230tTAoPckQdHNC{sFG~+L zU_#DJ5H;02{(~5%h-*T9C4ou)Ncsq%BHD5P3*Rk6Vjn?6gEj$IOd5_oa3PyY>?ZfY zO7R=>PXz&#_z9p(#%KAXGj9BGYjb4)LJ1{&9?45L0R ze`IVBJ&H#^(3>PdR%?lusHAcx2@HH%qvQc{_zgeWzvia^rj)-{JiHJ2EPx0URN&XA z?IwJyDNWB64MJ&x8V`@1l;I)QB&oCH1;9f7oW4qO$PT@YkK6?VgyYusm+U|!8+-{~ z#M+CT2_O6-d6oEq&H(eGKE+S1TmGY(x;&jmH-DvX;gdPVj}xaWIzu!%gKY4~<*Dr< zo0ovnp`Ih?Qh%+!lD|cMZX5f!dI^xb$CqIK;Kz-VY0>pC&d)+24R#;G@cDUJ+t`Y3 z^9apMR3w3#Y-}h7RQOj)pYSQRK?WsVp!m3z807>$WEd zk0`p(R7MuZ{jVr6ja(Z7#7}s>e);nC>oeu!N9FJdeI<1+@X%+1=nn~CSV@bY(y2Tc z5UQ)bQbxJ!xoIlFtx&^&x06(GDdcM|E#>zmq1rioF8#+n=I7|a=h@5^{t^rw!Keu*e%bm7oG3m&>r_d? z%#Vfvv97+jQSWu(_Jx!Ce9vEa4yoPXzLDV3{gVhX4T^3@_tX*3!D`&f{HT$!DWB_Z zJ_J$E{L733E~e7fc-}dL@JJzZxh7J6Ie$O~khdS${^5U>L!iJi7nxG~52ay1V(}Y% zsW(2NzqHprT7&5{@H;Fu0darK%VW4D7Ea-Q_WO)Hd3gzju-n*pk;uY~RVea_pjV1^ z>_30Oe@2`PzWKbK-m}8#wJS6l`izr$3VH)vk*7gF>1uZdhopV-#ZuoWIusMCNXtFI ze35>CrPN=l4=|-+a(Tl~ExYgYBQwbHR$m^3o6Yn^jD10}KK`^SS1De|5i zX6yH%zV!ZnS$GH3qpE3qFE5H_gl{wOrB@sA_AOx=zkXB;80+Mm1mDCh@oSUftJp~| zZ+YPz7`ix}HWxHQkqP)#csSbXkAO+S03Pi04f-0eTCHpZaUPVl9KksLXj%Zj!;b|1 zn)%%$f7$+J`SanykCz~PUY^}cf4h@M_^s$td54FFuG7p)@WhQbl4yx`lBJa)2oL3Z z%1ly+Zfy9qTAG#H;B`H`9V*^h_K+AoPaX$@XU?8NIK^k}@{2S#MkUKNudi`BxgVQ2 zC0mGb6d>y=#gV(Px>~sj_uPETfc016N6D}dJ&FV{uPM59t)~nVI$A@YGa!fbrT& zy*5?lT)~oV?MG!HOpj&^$E$bQ1rdIG zfTAzeOl28%ehx&m49XU)U?-TefIP7;KLI?qRXF7veursG;XRZTM1cs>`(LqbLd-#i$yK*(SszcC^LX!FyT z%o+Kj-(aA~{}w}z- zipu8rf-f{*{0iFO)61%ikLXV7`}~}*I6lF&u%eyBkqnIaF%Iue zsJ;bQ;q`Vl6!?nhjWkTg2g8MLf@RwbG81i$`K^K;%h3a+d%(ZEZ-U1WaMh3o-yxJ) z;>XV`wzODB|*iv{F0f1l4c0#bu8C{`FrO;4IeaH-S4En>OQk= z3olv%Z1Kz8G~zf7N-O!KB>D3g^1vG4BlMH}(WePMY#Q?S<_a3)#TA*~mDE?w_OHB> z(YpG_0eI_^G6vy!{Dn3vXHw-Bzv)+6!Wxrn_AnSk>^*!)pps*3RQ|={r)CAxr|6i$ zyCpo$X=UP<1|R^?`di^gFDH9O&m}Dnp4b#V0QNh*H6-?*6dz(Nc)=W?VhYRDhq zpUMZiFg0-U|10ebmKp|uFs$|BjFY3IwR)$)qR#mdhA)hq zk+47Ii`F{d6+f#Zy%Lw!59ejJ3~~i#bm^!$7z*Jo$y}VZ#KFEp!SBPGMIJp$jvM0V z^tN=WgIiX)h{aIiZA|}jF?XPy>T#z`RoM8tVigjADr*?p^5>=43qYGnFtIF|Z5d8Y zgU*=3v-vX7r!GXJy(9T)J`iIzJj1$Ku{3^;&-gt(z;BH#jo&4n8^XsvPKW(%Rox&Eb*RoANAW8MUmbj}_xv|B zmowtH+YyD|dSsIR+FE}ESeIm%V82byO*uK4kK1=1cjY$HCb3Xi&aaM$)8*&eCx4X&o}OO6{h_N%H|oBryIQ}L_2$=N_egq`0#l+0&( z8iI_SzaV+nyH9-lF)6G3jFVGU22-f!YDpV_TSRptqOxykGs#a~u=bJwoSAR~P%9v0 zBz^`^8@&)7Ykn?2av3^DJ&H_;G^d-ya@8a~5$|g#(3)6g%5lRt5FqnAh_97jr{Hh0 zHXAK7eZh2vY()9zKDV*76VT!J)P>!%0+ll@-*xmI>MHF&xK`KA!f(tZIesjfu; literal 0 HcmV?d00001 diff --git a/assets/cow.bmp b/assets/cow.bmp new file mode 100644 index 0000000000000000000000000000000000000000..65050a5ee525288ecb7f14178b25f3c0d7820236 GIT binary patch literal 2390 zcmds1!D_-l6nsKf6i+23`UxHb3!X#CRS3xDY>>WwNa?NM$JpNcFZJd@{~(?BcB7)S z2TxsxeVch?H_VdRd79TlQq-8Is7KTnsz(WC0PoY0uK`-A$z)8!{Dr=$rT5n@y{&ij z@wuV>VMlG-5}B&Rklo-;hKF&2N*lw?!}G%C($wH=(^NPsw^n$QF>{}nM~+rF9tzK! zY&GMRe5lVSE{m43;1(nW;RL!hWA~4E$H7B_0)7dW!9|F}|D|!J zI_h^OHvO^t=U4rbnc-(hW_J1MGt=j%W~RN)&k&g*KXsMz%gpSuRLy(i^r!y+n)zpE Ny=(n3`zMb4`~gB*J$V2C literal 0 HcmV?d00001 diff --git a/assets/cow.xcf b/assets/cow.xcf new file mode 100644 index 0000000000000000000000000000000000000000..e63a063e76a6e078878276a21f5d619274af7ba0 GIT binary patch literal 6456 zcmdT{OK%%h6u$O6oOf)uOBV@YFbF83Nf+A&IsEx8c$k> zO_2~1LP)TvSh462KrDD{Sn(rRum`c}0(PhpVffC>+<1~UiN`33xYqUg=FWNDd(S!d zPTgo)N~cm&?&S*whISJDC-gY_m*_F{=g2>eu!))J@<^JR@XCi+o;kQ z+>f1|YCD_I+BO?T6^d(hPw9|?HswJ@y*arwd1dmlQfru+qbh}qih4J{&##!z-<_UT zmHc@qT#KO47xXmm2LsI=nz+%b!y(w&-0>7VWHwrcqZqBqy50s)$E?-bx&z0+AHqJ| z$xI7$GPGz--O;L=qXi2mt(E6H&4zMMx7&u0U^OK6+s+d){G&_0w)|;7`$@#gV zG8de~nwA}{uC%nKKC8abF>2YXv>(|khSOfw?G>$QHFR}uM!~O~1MA)*i-@XQMibn{ zeDU>C{!$6MQh2TO>U61??@{)KZPbmH*62rv=lC_fRd?3axx%I50VX~6cZ@1=AL!pr z9l^NaU{!Bp_aJb9^3|?@IyYU+6%OFP(=-3_y6KqN1v9IKSewc1&&YOd*7RMOlwPY&6YQ|HRLZ9ThEW3_2zx4E;l3qj=#um3Hc zAP0NbY3}D&N3(kJ2wJkfALLO_u5=3oO^Fm-4+_tJQGudV+w4QZRm-VlFI!f_sA!as z#iy4xlfRTHTzpnqJS|M&xVCOrTP~^6hS~s~wrHuZeain&J>m$IANdmS=;cNu zYvFvTnARro$mn#0(>Z^R@cf7P$1#siv=f%U| znm;~5b3hyW*{6)HFj*dEf*gy*Ab#O9go4zqDL&Qb`zG-ySGe3cz7#nU(=z)zhaN{^!-Fu40P+Iq&<*c>y!BUik0KT&CV8>gYYd0a1@FQ{H>dZ4 z8w6d;@&V>I4jPlMX`8xDFM_+ufp;~!4E|s4YIL^?we*jn2M+ma1oKb-?!P$oNkAWR zMo`WRcp|v>o%qFnfASm7hjToEb3B1_Jb`l@J(a+j9N@HvQEw91uLKezf!db9IgaOK zz~}Stlu7R5k7ff27Re`(J&>4-svg0s(F7tjk`y z{v3su$al}nepb=nFGS!$IX}LlH}nd9Q7YH9_WKmGUabJ-A88MO{f{(y+xhl1HO#A)*DE^y?*xPC_I#06b^XU&kEwI IzDHjE3q{NN>i_@% literal 0 HcmV?d00001 diff --git a/assets/cow_m.bmp b/assets/cow_m.bmp new file mode 100644 index 0000000000000000000000000000000000000000..9f52e24cbfc4308233c4db0a97afd35a4daea84a GIT binary patch literal 2390 zcmds1F-`+P40I$U5>bLgo+I0H>M2EyMlHpW(#r{F zs5#{zAo7sY#7ER0`Bk&xsq1f?8J8UUJ9Yi+KV5sS_!RW>?5bwPQ`dj$=O@lNd)`=! z-=Fc1?_3+$819=JaB}wGJ^-~cpfBvjI2>cC071Q+#MGRn2Aa3@^5;FNYuQ=6Bn8Kd z`I~s8{XhM+6S5cCf3!w>t~fc`^8xOIkthS$Zz;y%7^|VGS#dp*AryJDELicGB*n^U I;v=4=U!l`kF8}}l literal 0 HcmV?d00001 diff --git a/assets/flaunch.pcm b/assets/flaunch.pcm new file mode 100644 index 0000000000000000000000000000000000000000..a6b15c6f9a0376cbf33f45a894964e6ab40711e6 GIT binary patch literal 4502 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 literal 0 HcmV?d00001 diff --git a/assets/font1.bmp b/assets/font1.bmp new file mode 100644 index 0000000000000000000000000000000000000000..04af59e6ae44a901acc895bb36b5f544b74375e7 GIT binary patch literal 1086 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 Mjy2!0|5D%m1M}JcPyhe` literal 0 HcmV?d00001 diff --git a/assets/witch_m.bmp b/assets/witch_m.bmp new file mode 100644 index 0000000000000000000000000000000000000000..b770be9500d15cfd99831cc7ee20a92c39faf869 GIT binary patch literal 1270 zcmZ?r{l>xo24z4}1BfMnI01+m86Xl3K;&{~y2% zAn_jvKx~izOdKH#k_X8`07x554x|qVz~%t;fdE_;kPS5-Dg)+$)%<7R2XY|7$ZDW$ zuye4n!8)K8Ky>4@4dzz3lc0{k5Jz + +class Audio { + Audio(); + explicit Audio(const char *path); + ~Audio(); + + [[nodiscard]] bool isValid() const; + void loadFromFile(const char* path); + void unload(); +private: + friend class AudioPlayer; + [[nodiscard]] void *getData() const { return _data; } + size_t _length{0}; + void *_data{nullptr}; +}; + + +#endif //GAME_AUDIO_H \ No newline at end of file diff --git a/audio/AudioPlayer.cpp b/audio/AudioPlayer.cpp new file mode 100644 index 0000000..4283214 --- /dev/null +++ b/audio/AudioPlayer.cpp @@ -0,0 +1,120 @@ + +#include "AudioPlayer.h" + +#include "../system/Opl.h" +#include "../system/Timer.h" + +#include "../util/Log.h" + +AudioPlayer audioPlayer; + +void radPlayerWriteReg(void *p, uint16_t reg, uint8_t data) { + auto player = static_cast(p); + player->writeOpl(reg, data); +} + +void audioPlayerOnTimer() { + audioPlayer.onTimer(); +} + +void audioPlayerOnTimerDelayed() { + DefaultLog.log("audioPlayerOnTimerDelayed()\n"); + static int idle = 10; + if (--idle) return; + Timer::instance().setCallback(audioPlayerOnTimer); +} + +AudioPlayer::AudioPlayer() { + Timer::instance().setCallback(audioPlayerOnTimerDelayed); +} + +AudioPlayer::~AudioPlayer() { + Timer::instance().setCallback(nullptr); +} + +void AudioPlayer::playMusic(Music &music) { + stopMusic(); + + _currentMusic = &music; + _musicPlayer.Init(_currentMusic->getData(), radPlayerWriteReg, this); + + auto hz = _musicPlayer.GetHertz(); + if (hz < 0) { + stopMusic(); + return; + } + + // Start music playback + Timer::instance().setFrequency(hz); + resumeMusic(); +} + +void AudioPlayer::stopMusic() { + if (!_currentMusic) return; + + _musicPlayer.Stop(); + _paused = true; + _currentMusic = nullptr; +} + +void AudioPlayer::pauseMusic() { + _paused = true; +} + +void AudioPlayer::resumeMusic() { + if (!_currentMusic) return; + + _paused = false; +} + +void AudioPlayer::playAudio(Audio &audio, int priority, int flags) { + if (priority > AUDIO_MAX_PRIORITY) priority = AUDIO_MAX_PRIORITY; + + +} + +bool AudioPlayer::isPlaying(const Audio &audio) { + for (auto &channel : _channels) { + if (channel.audio == &audio) return true; + } + return false; +} + +void AudioPlayer::stopAudio(Audio &audio) { + for (auto &channel : _channels) { + if (channel.audio == &audio) { + channel.audio = nullptr; + channel.loop = false; + } + } +} + +void AudioPlayer::stopAllAudio() { + for (auto &channel : _channels) { + channel.audio = nullptr; + channel.loop = false; + } +} + +void AudioPlayer::generateSamples(uint8_t *buffer, size_t size) { +} + +bool AudioPlayer::isPlaying(const Music &music) const { + return _currentMusic == &music; +} + +void AudioPlayer::writeOpl(uint16_t reg, uint8_t data) const { + Opl::write(reg, data); +} + +void AudioPlayer::onTimer() { + if (_paused) return; + if (!_currentMusic) return; + + _musicPlayer.Update(); +} + +void AudioPlayer::generateSamples() { + +} + diff --git a/audio/AudioPlayer.h b/audio/AudioPlayer.h new file mode 100644 index 0000000..1f53765 --- /dev/null +++ b/audio/AudioPlayer.h @@ -0,0 +1,64 @@ +#ifndef GAME_AUDIOPLAYER_H +#define GAME_AUDIOPLAYER_H + +#include + +#include "Audio.h" +#include "Music.h" +#include "rad20player.h" + +#define AUDIO_CHANNELS 8 +#define AUDIO_MAX_PRIORITY 7 + +#define AUDIO_PLAY_LOOP 0x01 +#define AUDIO_PLAY_FIXED_PRIORITY 0x02 + +class AudioPlayer { +public: + AudioPlayer(); + + ~AudioPlayer(); + + void playMusic(Music &music); + + void stopMusic(); + + void pauseMusic(); + + void resumeMusic(); + + void playAudio(Audio &audio, int priority, int flags = 0); + + bool isPlaying(const Audio & audio); + + void stopAudio(Audio &audio); + + void stopAllAudio(); + + void generateSamples(uint8_t *buffer, size_t size); +private: + friend class Music; + friend void radPlayerWriteReg(void *p, uint16_t reg, uint8_t data); + friend void audioPlayerOnTimer(); + + RADPlayer _musicPlayer; + Music *_currentMusic{nullptr}; + bool _paused{true}; + + struct { + Audio *audio{nullptr}; + bool loop{false}; + } _channels[AUDIO_CHANNELS]; + + [[nodiscard]] bool isPlaying(const Music &music) const; + void writeOpl(uint16_t reg, uint8_t data) const; + void onTimer(); + + void generateSamples(); + + void copySamples(); +}; + +extern AudioPlayer audioPlayer; + +#endif //GAME_AUDIOPLAYER_H diff --git a/audio/Music.cpp b/audio/Music.cpp new file mode 100644 index 0000000..0f077c9 --- /dev/null +++ b/audio/Music.cpp @@ -0,0 +1,34 @@ +#include "Music.h" + +#include "AudioPlayer.h" +#include "../util/Files.h" + +Music::Music() { +} + +Music::Music(const char *path) { + loadFromFile(path); +} + +Music::~Music() { + unload(); +} + +bool Music::isValid() const { + return _data != nullptr; +} + +void Music::loadFromFile(const char *path) { + unload(); + _length = Files::allocateBufferAndLoadFromFile(path, &_data); +} + +void Music::unload() { + if (_data) { + if (audioPlayer.isPlaying(*this)) { + audioPlayer.stopMusic(); + } + Files::deleteBuffer(_data); + _length = 0; + } +} diff --git a/audio/Music.h b/audio/Music.h new file mode 100644 index 0000000..9a98532 --- /dev/null +++ b/audio/Music.h @@ -0,0 +1,23 @@ +#ifndef GAME_MUSIC_H +#define GAME_MUSIC_H + +#include + +class Music { +public: + Music(); + explicit Music(const char *path); + ~Music(); + + [[nodiscard]] bool isValid() const; + void loadFromFile(const char* path); + void unload(); +private: + friend class AudioPlayer; + [[nodiscard]] void *getData() const { return _data; } + size_t _length{0}; + void *_data{nullptr}; +}; + + +#endif //GAME_MUSIC_H \ No newline at end of file diff --git a/audio/rad20player.cpp b/audio/rad20player.cpp new file mode 100644 index 0000000..71d33fe --- /dev/null +++ b/audio/rad20player.cpp @@ -0,0 +1,1097 @@ +/* + + C++ player code for Reality Adlib Tracker 2.0a (file version 2.1). + + Please note, this is just the player code. This does no checking of the tune data before + it tries to play it, as most use cases will be a known tune being used in a production. + So if you're writing an application that loads unknown tunes in at run time then you'll + want to do more validity checking. + + To use: + + - Instantiate the RADPlayer object + + - Initialise player for your tune by calling the Init() method. Supply a pointer to the + tune file and a function for writing to the OPL3 registers. + + - Call the Update() method a number of times per second as returned by GetHertz(). If + your tune is using the default BPM setting you can safely just call it 50 times a + second, unless it's a legacy "slow-timer" tune then it'll need to be 18.2 times a + second. + + - When you're done, stop calling Update() and call the Stop() method to turn off all + sound and reset the OPL3 hardware. + +*/ + +#include "rad20player.h" + +#ifndef RAD_DETECT_REPEATS +#define RAD_DETECT_REPEATS 0 +#endif + + +//-------------------------------------------------------------------------------------------------- +const int8_t RADPlayer::NoteSize[] = { 0, 2, 1, 3, 1, 3, 2, 4 }; +const uint16_t RADPlayer::ChanOffsets3[9] = { 0, 1, 2, 0x100, 0x101, 0x102, 6, 7, 8 }; // OPL3 first channel +const uint16_t RADPlayer::Chn2Offsets3[9] = { 3, 4, 5, 0x103, 0x104, 0x105, 0x106, 0x107, 0x108 }; // OPL3 second channel +const uint16_t RADPlayer::NoteFreq[] = { 0x16b,0x181,0x198,0x1b0,0x1ca,0x1e5,0x202,0x220,0x241,0x263,0x287,0x2ae }; +const uint16_t RADPlayer::OpOffsets3[9][4] = { + { 0x00B, 0x008, 0x003, 0x000 }, + { 0x00C, 0x009, 0x004, 0x001 }, + { 0x00D, 0x00A, 0x005, 0x002 }, + { 0x10B, 0x108, 0x103, 0x100 }, + { 0x10C, 0x109, 0x104, 0x101 }, + { 0x10D, 0x10A, 0x105, 0x102 }, + { 0x113, 0x110, 0x013, 0x010 }, + { 0x114, 0x111, 0x014, 0x011 }, + { 0x115, 0x112, 0x015, 0x012 } +}; +const bool RADPlayer::AlgCarriers[7][4] = { + { true, false, false, false }, // 0 - 2op - op < op + { true, true, false, false }, // 1 - 2op - op + op + { true, false, false, false }, // 2 - 4op - op < op < op < op + { true, false, false, true }, // 3 - 4op - op < op < op + op + { true, false, true, false }, // 4 - 4op - op < op + op < op + { true, false, true, true }, // 5 - 4op - op < op + op + op + { true, true, true, true }, // 6 - 4op - op + op + op + op +}; + + + +//================================================================================================== +// Initialise a RAD tune for playback. This assumes the tune data is valid and does minimal data +// checking. +//================================================================================================== +void RADPlayer::Init(const void *tune, void (*opl3)(void *, uint16_t, uint8_t), void *arg) { + + Initialised = false; + + // Version check; we only support version 2.1 tune files + if (*((uint8_t *)tune + 0x10) != 0x21) { + Hertz = -1; + return; + } + + // The OPL3 call-back + OPL3 = opl3; + OPL3Arg = arg; + + for (int i = 0; i < kTracks; i++) + Tracks[i] = 0; + + for (int i = 0; i < kRiffTracks; i++) + for (int j = 0; j < kChannels; j++) + Riffs[i][j] = 0; + + uint8_t *s = (uint8_t *)tune + 0x11; + + uint8_t flags = *s++; + Speed = flags & 0x1F; + + // Is BPM value present? + Hertz = 50; + if (flags & 0x20) { + Hertz = (s[0] | (int(s[1]) << 8)) * 2 / 5; + s += 2; + } + + // Slow timer tune? Return an approximate hz + if (flags & 0x40) + Hertz = 18; + + // Skip any description + while (*s) + s++; + s++; + + // Unpack the instruments + while (1) { + + // Instrument number, 0 indicates end of list + uint8_t inst_num = *s++; + if (inst_num == 0) + break; + + // Skip instrument name + s += *s++; + + CInstrument &inst = Instruments[inst_num - 1]; + + uint8_t alg = *s++; + inst.Algorithm = alg & 7; + inst.Panning[0] = (alg >> 3) & 3; + inst.Panning[1] = (alg >> 5) & 3; + + if (inst.Algorithm < 7) { + + uint8_t b = *s++; + inst.Feedback[0] = b & 15; + inst.Feedback[1] = b >> 4; + + b = *s++; + inst.Detune = b >> 4; + inst.RiffSpeed = b & 15; + + inst.Volume = *s++; + + for (int i = 0; i < 4; i++) { + uint8_t *op = inst.Operators[i]; + for (int j = 0; j < 5; j++) + op[j] = *s++; + } + + } else { + + // Ignore MIDI instrument data + s += 6; + } + + // Instrument riff? + if (alg & 0x80) { + int size = s[0] | (int(s[1]) << 8); + s += 2; + inst.Riff = s; + s += size; + } else + inst.Riff = 0; + } + + // Get order list + OrderListSize = *s++; + OrderList = s; + s += OrderListSize; + + // Locate the tracks + while (1) { + + // Track number + uint8_t track_num = *s++; + if (track_num >= kTracks) + break; + + // Track size in bytes + int size = s[0] | (int(s[1]) << 8); + s += 2; + + Tracks[track_num] = s; + s += size; + } + + // Locate the riffs + while (1) { + + // Riff id + uint8_t riffid = *s++; + uint8_t riffnum = riffid >> 4; + uint8_t channum = riffid & 15; + if (riffnum >= kRiffTracks || channum > kChannels) + break; + + // Track size in bytes + int size = s[0] | (int(s[1]) << 8); + s += 2; + + Riffs[riffnum][channum - 1] = s; + s += size; + } + + // Done parsing tune, now set up for play + for (int i = 0; i < 512; i++) + OPL3Regs[i] = 255; + Stop(); + + Initialised = true; +} + + + +//================================================================================================== +// Stop all sounds and reset the tune. Tune will play from the beginning again if you continue to +// Update(). +//================================================================================================== +void RADPlayer::Stop() { + + // Clear all registers + for (uint16_t reg = 0x20; reg < 0xF6; reg++) { + + // Ensure envelopes decay all the way + uint8_t val = (reg >= 0x60 && reg < 0xA0) ? 0xFF : 0; + + SetOPL3(reg, val); + SetOPL3(reg + 0x100, val); + } + + // Configure OPL3 + SetOPL3(1, 0x20); // Allow waveforms + SetOPL3(8, 0); // No split point + SetOPL3(0xbd, 0); // No drums, etc. + SetOPL3(0x104, 0); // Everything 2-op by default + SetOPL3(0x105, 1); // OPL3 mode on + +#if RAD_DETECT_REPEATS + // The order map keeps track of which patterns we've played so we can detect when the tune + // starts to repeat. Jump markers can't be reliably used for this + PlayTime = 0; + Repeating = false; + for (int i = 0; i < 4; i++) + OrderMap[i] = 0; +#endif + + // Initialise play values + SpeedCnt = 1; + Order = 0; + Track = GetTrack(); + Line = 0; + Entrances = 0; + MasterVol = 64; + + // Initialise channels + for (int i = 0; i < kChannels; i++) { + CChannel &chan = Channels[i]; + chan.LastInstrument = 0; + chan.Instrument = 0; + chan.Volume = 0; + chan.DetuneA = 0; + chan.DetuneB = 0; + chan.KeyFlags = 0; + chan.Riff.SpeedCnt = 0; + chan.IRiff.SpeedCnt = 0; + } +} + + + +//================================================================================================== +// Playback update. Call BPM * 2 / 5 times a second. Use GetHertz() for this number after the +// tune has been initialised. Returns true if tune is starting to repeat. +//================================================================================================== +bool RADPlayer::Update() { + + if (!Initialised) + return false; + + // Run riffs + for (int i = 0; i < kChannels; i++) { + CChannel &chan = Channels[i]; + TickRiff(i, chan.IRiff, false); + TickRiff(i, chan.Riff, true); + } + + // Run main track + PlayLine(); + + // Run effects + for (int i = 0; i < kChannels; i++) { + CChannel &chan = Channels[i]; + ContinueFX(i, &chan.IRiff.FX); + ContinueFX(i, &chan.Riff.FX); + ContinueFX(i, &chan.FX); + } + + // Update play time. We convert to seconds when queried + PlayTime++; + +#if RAD_DETECT_REPEATS + return Repeating; +#else + return false; +#endif +} + + + +//================================================================================================== +// Unpacks a single RAD note. +//================================================================================================== +bool RADPlayer::UnpackNote(uint8_t *&s, uint8_t &last_instrument) { + + uint8_t chanid = *s++; + + InstNum = 0; + EffectNum = 0; + Param = 0; + + // Unpack note data + uint8_t note = 0; + if (chanid & 0x40) { + uint8_t n = *s++; + note = n & 0x7F; + + // Retrigger last instrument? + if (n & 0x80) + InstNum = last_instrument; + } + + // Do we have an instrument? + if (chanid & 0x20) { + InstNum = *s++; + last_instrument = InstNum; + } + + // Do we have an effect? + if (chanid & 0x10) { + EffectNum = *s++; + Param = *s++; + } + + NoteNum = note & 15; + OctaveNum = note >> 4; + + return ((chanid & 0x80) != 0); +} + + + +//================================================================================================== +// Get current track as indicated by order list. +//================================================================================================== +uint8_t *RADPlayer::GetTrack() { + + // If at end of tune start again from beginning + if (Order >= OrderListSize) + Order = 0; + + uint8_t track_num = OrderList[Order]; + + // Jump marker? Note, we don't recognise multiple jump markers as that could put us into an + // infinite loop + if (track_num & 0x80) { + Order = track_num & 0x7F; + track_num = OrderList[Order] & 0x7F; + } + +#if RAD_DETECT_REPEATS + // Check for tune repeat, and mark order in order map + if (Order < 128) { + int byte = Order >> 5; + uint32_t bit = uint32_t(1) << (Order & 31); + if (OrderMap[byte] & bit) + Repeating = true; + else + OrderMap[byte] |= bit; + } +#endif + + return Tracks[track_num]; +} + + + +//================================================================================================== +// Skip through track till we reach the given line or the next higher one. Returns null if none. +//================================================================================================== +uint8_t *RADPlayer::SkipToLine(uint8_t *trk, uint8_t linenum, bool chan_riff) { + + while (1) { + + uint8_t lineid = *trk; + if ((lineid & 0x7F) >= linenum) + return trk; + if (lineid & 0x80) + break; + trk++; + + // Skip channel notes + uint8_t chanid; + do { + chanid = *trk++; + trk += NoteSize[(chanid >> 4) & 7]; + } while (!(chanid & 0x80) && !chan_riff); + } + + return 0; +} + + + +//================================================================================================== +// Plays one line of current track and advances pointers. +//================================================================================================== +void RADPlayer::PlayLine() { + + SpeedCnt--; + if (SpeedCnt > 0) + return; + SpeedCnt = Speed; + + // Reset channel effects + for (int i = 0; i < kChannels; i++) + ResetFX(&Channels[i].FX); + + LineJump = -1; + + // At the right line? + uint8_t *trk = Track; + if (trk && (*trk & 0x7F) <= Line) { + uint8_t lineid = *trk++; + + // Run through channels + bool last; + do { + int channum = *trk & 15; + CChannel &chan = Channels[channum]; + last = UnpackNote(trk, chan.LastInstrument); + PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param); + } while (!last); + + // Was this the last line? + if (lineid & 0x80) + trk = 0; + + Track = trk; + } + + // Move to next line + Line++; + if (Line >= kTrackLines || LineJump >= 0) { + + if (LineJump >= 0) + Line = LineJump; + else + Line = 0; + + // Move to next track in order list + Order++; + Track = GetTrack(); + } +} + + + +//================================================================================================== +// Play a single note. Returns the line number in the next pattern to jump to if a jump command was +// found, or -1 if none. +//================================================================================================== +void RADPlayer::PlayNote(int channum, int8_t notenum, int8_t octave, uint16_t instnum, uint8_t cmd, uint8_t param, e_Source src, int op) { + CChannel &chan = Channels[channum]; + + // Recursion detector. This is needed as riffs can trigger other riffs, and they could end up + // in a loop + if (Entrances >= 8) + return; + Entrances++; + + // Select which effects source we're using + CEffects *fx = &chan.FX; + if (src == SRiff) + fx = &chan.Riff.FX; + else if (src == SIRiff) + fx = &chan.IRiff.FX; + + bool transposing = false; + + // For tone-slides the note is the target + if (cmd == cmToneSlide) { + if (notenum > 0 && notenum <= 12) { + fx->ToneSlideOct = octave; + fx->ToneSlideFreq = NoteFreq[notenum - 1]; + } + goto toneslide; + } + + // Playing a new instrument? + if (instnum > 0) { + CInstrument *oldinst = chan.Instrument; + CInstrument *inst = &Instruments[instnum - 1]; + chan.Instrument = inst; + + // Ignore MIDI instruments + if (inst->Algorithm == 7) { + Entrances--; + return; + } + + LoadInstrumentOPL3(channum); + + // Bounce the channel + chan.KeyFlags |= fKeyOff | fKeyOn; + + ResetFX(&chan.IRiff.FX); + + if (src != SIRiff || inst != oldinst) { + + // Instrument riff? + if (inst->Riff && inst->RiffSpeed > 0) { + + chan.IRiff.Track = chan.IRiff.TrackStart = inst->Riff; + chan.IRiff.Line = 0; + chan.IRiff.Speed = inst->RiffSpeed; + chan.IRiff.LastInstrument = 0; + + // Note given with riff command is used to transpose the riff + if (notenum >= 1 && notenum <= 12) { + chan.IRiff.TransposeOctave = octave; + chan.IRiff.TransposeNote = notenum; + transposing = true; + } else { + chan.IRiff.TransposeOctave = 3; + chan.IRiff.TransposeNote = 12; + } + + // Do first tick of riff + chan.IRiff.SpeedCnt = 1; + TickRiff(channum, chan.IRiff, false); + + } else + chan.IRiff.SpeedCnt = 0; + } + } + + // Starting a channel riff? + if (cmd == cmRiff || cmd == cmTranspose) { + + ResetFX(&chan.Riff.FX); + + uint8_t p0 = param / 10; + uint8_t p1 = param % 10; + chan.Riff.Track = p1 > 0 ? Riffs[p0][p1 - 1] : 0; + if (chan.Riff.Track) { + + chan.Riff.TrackStart = chan.Riff.Track; + chan.Riff.Line = 0; + chan.Riff.Speed = Speed; + chan.Riff.LastInstrument = 0; + + // Note given with riff command is used to transpose the riff + if (cmd == cmTranspose && notenum >= 1 && notenum <= 12) { + chan.Riff.TransposeOctave = octave; + chan.Riff.TransposeNote = notenum; + transposing = true; + } else { + chan.Riff.TransposeOctave = 3; + chan.Riff.TransposeNote = 12; + } + + // Do first tick of riff + chan.Riff.SpeedCnt = 1; + TickRiff(channum, chan.Riff, true); + + } else + chan.Riff.SpeedCnt = 0; + } + + // Play the note + if (!transposing && notenum > 0) { + + // Key-off? + if (notenum == 15) + chan.KeyFlags |= fKeyOff; + + if (!chan.Instrument || chan.Instrument->Algorithm < 7) + PlayNoteOPL3(channum, octave, notenum); + } + + // Process effect + switch (cmd) { + + case cmSetVol: + SetVolume(channum, param); + break; + + case cmSetSpeed: + if (src == SNone) { + Speed = param; + SpeedCnt = param; + } else if (src == SRiff) { + chan.Riff.Speed = param; + chan.Riff.SpeedCnt = param; + } else if (src == SIRiff) { + chan.IRiff.Speed = param; + chan.IRiff.SpeedCnt = param; + } + break; + + case cmPortamentoUp: + fx->PortSlide = param; + break; + + case cmPortamentoDwn: + fx->PortSlide = -int8_t(param); + break; + + case cmToneVolSlide: + case cmVolSlide: { + int8_t val = param; + if (val >= 50) + val = -(val - 50); + fx->VolSlide = val; + if (cmd != cmToneVolSlide) + break; + } + // Fall through! + + case cmToneSlide: { +toneslide: + uint8_t speed = param; + if (speed) + fx->ToneSlideSpeed = speed; + GetSlideDir(channum, fx); + break; + } + + case cmJumpToLine: { + if (param >= kTrackLines) + break; + + // Note: jump commands in riffs are checked for within TickRiff() + if (src == SNone) + LineJump = param; + + break; + } + + case cmMultiplier: { + if (src == SIRiff) + LoadInstMultiplierOPL3(channum, op, param); + break; + } + + case cmVolume: { + if (src == SIRiff) + LoadInstVolumeOPL3(channum, op, param); + break; + } + + case cmFeedback: { + if (src == SIRiff) { + uint8_t which = param / 10; + uint8_t fb = param % 10; + LoadInstFeedbackOPL3(channum, which, fb); + } + break; + } + } + + Entrances--; +} + + + +//================================================================================================== +// Sets the OPL3 registers for a given instrument. +//================================================================================================== +void RADPlayer::LoadInstrumentOPL3(int channum) { + CChannel &chan = Channels[channum]; + + const CInstrument *inst = chan.Instrument; + if (!inst) + return; + + uint8_t alg = inst->Algorithm; + chan.Volume = inst->Volume; + chan.DetuneA = (inst->Detune + 1) >> 1; + chan.DetuneB = inst->Detune >> 1; + + // Turn on 4-op mode for algorithms 2 and 3 (algorithms 4 to 6 are simulated with 2-op mode) + if (channum < 6) { + uint8_t mask = 1 << channum; + SetOPL3(0x104, (GetOPL3(0x104) & ~mask) | (alg == 2 || alg == 3 ? mask : 0)); + } + + // Left/right/feedback/algorithm + SetOPL3(0xC0 + ChanOffsets3[channum], ((inst->Panning[1] ^ 3) << 4) | inst->Feedback[1] << 1 | (alg == 3 || alg == 5 || alg == 6 ? 1 : 0)); + SetOPL3(0xC0 + Chn2Offsets3[channum], ((inst->Panning[0] ^ 3) << 4) | inst->Feedback[0] << 1 | (alg == 1 || alg == 6 ? 1 : 0)); + + // Load the operators + for (int i = 0; i < 4; i++) { + + static const uint8_t blank[] = { 0, 0x3F, 0, 0xF0, 0 }; + const uint8_t *op = (alg < 2 && i >= 2) ? blank : inst->Operators[i]; + uint16_t reg = OpOffsets3[channum][i]; + + uint16_t vol = ~op[1] & 0x3F; + + // Do volume scaling for carriers + if (AlgCarriers[alg][i]) { + vol = vol * inst->Volume / 64; + vol = vol * MasterVol / 64; + } + + SetOPL3(reg + 0x20, op[0]); + SetOPL3(reg + 0x40, (op[1] & 0xC0) | ((vol ^ 0x3F) & 0x3F)); + SetOPL3(reg + 0x60, op[2]); + SetOPL3(reg + 0x80, op[3]); + SetOPL3(reg + 0xE0, op[4]); + } +} + + + +//================================================================================================== +// Play note on OPL3 hardware. +//================================================================================================== +void RADPlayer::PlayNoteOPL3(int channum, int8_t octave, int8_t note) { + CChannel &chan = Channels[channum]; + + uint16_t o1 = ChanOffsets3[channum]; + uint16_t o2 = Chn2Offsets3[channum]; + + // Key off the channel + if (chan.KeyFlags & fKeyOff) { + chan.KeyFlags &= ~(fKeyOff | fKeyedOn); + SetOPL3(0xB0 + o1, GetOPL3(0xB0 + o1) & ~0x20); + SetOPL3(0xB0 + o2, GetOPL3(0xB0 + o2) & ~0x20); + } + + if (note == 15) + return; + + bool op4 = (chan.Instrument && chan.Instrument->Algorithm >= 2); + + uint16_t freq = NoteFreq[note - 1]; + uint16_t frq2 = freq; + + chan.CurrFreq = freq; + chan.CurrOctave = octave; + + // Detune. We detune both channels in the opposite direction so the note retains its tuning + freq += chan.DetuneA; + frq2 -= chan.DetuneB; + + // Frequency low byte + if (op4) + SetOPL3(0xA0 + o1, frq2 & 0xFF); + SetOPL3(0xA0 + o2, freq & 0xFF); + + // Frequency high bits + octave + key on + if (chan.KeyFlags & fKeyOn) + chan.KeyFlags = (chan.KeyFlags & ~fKeyOn) | fKeyedOn; + if (op4) + SetOPL3(0xB0 + o1, (frq2 >> 8) | (octave << 2) | ((chan.KeyFlags & fKeyedOn) ? 0x20 : 0)); + else + SetOPL3(0xB0 + o1, 0); + SetOPL3(0xB0 + o2, (freq >> 8) | (octave << 2) | ((chan.KeyFlags & fKeyedOn) ? 0x20 : 0)); +} + + + +//================================================================================================== +// Prepare FX for new line. +//================================================================================================== +void RADPlayer::ResetFX(CEffects *fx) { + fx->PortSlide = 0; + fx->VolSlide = 0; + fx->ToneSlideDir = 0; +} + + + +//================================================================================================== +// Tick the channel riff. +//================================================================================================== +void RADPlayer::TickRiff(int channum, CChannel::CRiff &riff, bool chan_riff) { + uint8_t lineid; + + if (riff.SpeedCnt == 0) { + ResetFX(&riff.FX); + return; + } + + riff.SpeedCnt--; + if (riff.SpeedCnt > 0) + return; + riff.SpeedCnt = riff.Speed; + + uint8_t line = riff.Line++; + if (riff.Line >= kTrackLines) + riff.SpeedCnt = 0; + + ResetFX(&riff.FX); + + // Is this the current line in track? + uint8_t *trk = riff.Track; + if (trk && (*trk & 0x7F) == line) { + lineid = *trk++; + + if (chan_riff) { + + // Channel riff: play current note + UnpackNote(trk, riff.LastInstrument); + Transpose(riff.TransposeNote, riff.TransposeOctave); + PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param, SRiff); + + } else { + + // Instrument riff: here each track channel is an extra effect that can run, but is not + // actually a different physical channel + bool last; + do { + int col = *trk & 15; + last = UnpackNote(trk, riff.LastInstrument); + if (EffectNum != cmIgnore) + Transpose(riff.TransposeNote, riff.TransposeOctave); + PlayNote(channum, NoteNum, OctaveNum, InstNum, EffectNum, Param, SIRiff, col > 0 ? (col - 1) & 3 : 0); + } while (!last); + } + + // Last line? + if (lineid & 0x80) + trk = 0; + + riff.Track = trk; + } + + // Special case; if next line has a jump command, run it now + if (!trk || (*trk++ & 0x7F) != riff.Line) + return; + + UnpackNote(trk, lineid); // lineid is just a dummy here + if (EffectNum == cmJumpToLine && Param < kTrackLines) { + riff.Line = Param; + riff.Track = SkipToLine(riff.TrackStart, Param, chan_riff); + } +} + + + +//================================================================================================== +// This continues any effects that operate continuously (eg. slides). +//================================================================================================== +void RADPlayer::ContinueFX(int channum, CEffects *fx) { + CChannel &chan = Channels[channum]; + + if (fx->PortSlide) + Portamento(channum, fx, fx->PortSlide, false); + + if (fx->VolSlide) { + int8_t vol = chan.Volume; + vol -= fx->VolSlide; + if (vol < 0) + vol = 0; + SetVolume(channum, vol); + } + + if (fx->ToneSlideDir) + Portamento(channum, fx, fx->ToneSlideDir, true); +} + + + +//================================================================================================== +// Sets the volume of given channel. +//================================================================================================== +void RADPlayer::SetVolume(int channum, uint8_t vol) { + CChannel &chan = Channels[channum]; + + // Ensure volume is within range + if (vol > 64) + vol = 64; + + chan.Volume = vol; + + // Scale volume to master volume + vol = vol * MasterVol / 64; + + CInstrument *inst = chan.Instrument; + if (!inst) + return; + uint8_t alg = inst->Algorithm; + + // Set volume of all carriers + for (int i = 0; i < 4; i++) { + uint8_t *op = inst->Operators[i]; + + // Is this operator a carrier? + if (!AlgCarriers[alg][i]) + continue; + + uint8_t opvol = uint16_t((op[1] & 63) ^ 63) * vol / 64; + uint16_t reg = 0x40 + OpOffsets3[channum][i]; + SetOPL3(reg, (GetOPL3(reg) & 0xC0) | (opvol ^ 0x3F)); + } +} + + + +//================================================================================================== +// Starts a tone-slide. +//================================================================================================== +void RADPlayer::GetSlideDir(int channum, CEffects *fx) { + CChannel &chan = Channels[channum]; + + int8_t speed = fx->ToneSlideSpeed; + if (speed > 0) { + uint8_t oct = fx->ToneSlideOct; + uint16_t freq = fx->ToneSlideFreq; + + uint16_t oldfreq = chan.CurrFreq; + uint8_t oldoct = chan.CurrOctave; + + if (oldoct > oct) + speed = -speed; + else if (oldoct == oct) { + if (oldfreq > freq) + speed = -speed; + else if (oldfreq == freq) + speed = 0; + } + } + + fx->ToneSlideDir = speed; +} + + + +//================================================================================================== +// Load multiplier value into operator. +//================================================================================================== +void RADPlayer::LoadInstMultiplierOPL3(int channum, int op, uint8_t mult) { + uint16_t reg = 0x20 + OpOffsets3[channum][op]; + SetOPL3(reg, (GetOPL3(reg) & 0xF0) | (mult & 15)); +} + + + +//================================================================================================== +// Load volume value into operator. +//================================================================================================== +void RADPlayer::LoadInstVolumeOPL3(int channum, int op, uint8_t vol) { + uint16_t reg = 0x40 + OpOffsets3[channum][op]; + SetOPL3(reg, (GetOPL3(reg) & 0xC0) | ((vol & 0x3F) ^ 0x3F)); +} + + + +//================================================================================================== +// Load feedback value into instrument. +//================================================================================================== +void RADPlayer::LoadInstFeedbackOPL3(int channum, int which, uint8_t fb) { + + if (which == 0) { + + uint16_t reg = 0xC0 + Chn2Offsets3[channum]; + SetOPL3(reg, (GetOPL3(reg) & 0x31) | ((fb & 7) << 1)); + + } else if (which == 1) { + + uint16_t reg = 0xC0 + ChanOffsets3[channum]; + SetOPL3(reg, (GetOPL3(reg) & 0x31) | ((fb & 7) << 1)); + } +} + + + +//================================================================================================== +// This adjusts the pitch of the given channel's note. There may also be a limiting value on the +// portamento (for tone slides). +//================================================================================================== +void RADPlayer::Portamento(uint16_t channum, CEffects *fx, int8_t amount, bool toneslide) { + CChannel &chan = Channels[channum]; + + uint16_t freq = chan.CurrFreq; + uint8_t oct = chan.CurrOctave; + + freq += amount; + + if (freq < 0x156) { + + if (oct > 0) { + oct--; + freq += 0x2AE - 0x156; + } else + freq = 0x156; + + } else if (freq > 0x2AE) { + + if (oct < 7) { + oct++; + freq -= 0x2AE - 0x156; + } else + freq = 0x2AE; + } + + if (toneslide) { + + if (amount >= 0) { + + if (oct > fx->ToneSlideOct || (oct == fx->ToneSlideOct && freq >= fx->ToneSlideFreq)) { + freq = fx->ToneSlideFreq; + oct = fx->ToneSlideOct; + } + + } else { + + if (oct < fx->ToneSlideOct || (oct == fx->ToneSlideOct && freq <= fx->ToneSlideFreq)) { + freq = fx->ToneSlideFreq; + oct = fx->ToneSlideOct; + } + } + } + + chan.CurrFreq = freq; + chan.CurrOctave = oct; + + // Apply detunes + uint16_t frq2 = freq - chan.DetuneB; + freq += chan.DetuneA; + + // Write value back to OPL3 + uint16_t chan_offset = Chn2Offsets3[channum]; + SetOPL3(0xA0 + chan_offset, freq & 0xFF); + SetOPL3(0xB0 + chan_offset, (freq >> 8 & 3) | oct << 2 | (GetOPL3(0xB0 + chan_offset) & 0xE0)); + + chan_offset = ChanOffsets3[channum]; + SetOPL3(0xA0 + chan_offset, frq2 & 0xFF); + SetOPL3(0xB0 + chan_offset, (frq2 >> 8 & 3) | oct << 2 | (GetOPL3(0xB0 + chan_offset) & 0xE0)); +} + + + +//================================================================================================== +// Transpose the note returned by UnpackNote(). +// Note: due to RAD's wonky legacy middle C is octave 3 note number 12. +//================================================================================================== +void RADPlayer::Transpose(int8_t note, int8_t octave) { + + if (NoteNum >= 1 && NoteNum <= 12) { + + int8_t toct = octave - 3; + if (toct != 0) { + OctaveNum += toct; + if (OctaveNum < 0) + OctaveNum = 0; + else if (OctaveNum > 7) + OctaveNum = 7; + } + + int8_t tnot = note - 12; + if (tnot != 0) { + NoteNum += tnot; + if (NoteNum < 1) { + NoteNum += 12; + if (OctaveNum > 0) + OctaveNum--; + else + NoteNum = 1; + } + } + } +} + + + +//================================================================================================== +// Compute total time of tune if it didn't repeat. Note, this stops the tune so should only be done +// prior to initial playback. +//================================================================================================== +#if RAD_DETECT_REPEATS +static void RADPlayerDummyOPL3(void *arg, uint16_t reg, uint8_t data) {} +//-------------------------------------------------------------------------------------------------- +uint32_t RADPlayer::ComputeTotalTime() { + + Stop(); + void (*old_opl3)(void *, uint16_t, uint8_t) = OPL3; + OPL3 = RADPlayerDummyOPL3; + + while (!Update()) + ; + uint32_t total = PlayTime; + + Stop(); + OPL3 = old_opl3; + + return total / Hertz; +} +#endif + diff --git a/audio/rad20player.h b/audio/rad20player.h new file mode 100644 index 0000000..22c986e --- /dev/null +++ b/audio/rad20player.h @@ -0,0 +1,173 @@ +#ifndef RAD20PLAYER_H +#define RAD20PLAYER_H + +#include + +//================================================================================================== +// RAD player class. +//================================================================================================== +class RADPlayer { + + // Various constants + enum { + kTracks = 100, + kChannels = 9, + kTrackLines = 64, + kRiffTracks = 10, + kInstruments = 127, + + cmPortamentoUp = 0x1, + cmPortamentoDwn = 0x2, + cmToneSlide = 0x3, + cmToneVolSlide = 0x5, + cmVolSlide = 0xA, + cmSetVol = 0xC, + cmJumpToLine = 0xD, + cmSetSpeed = 0xF, + cmIgnore = ('I' - 55), + cmMultiplier = ('M' - 55), + cmRiff = ('R' - 55), + cmTranspose = ('T' - 55), + cmFeedback = ('U' - 55), + cmVolume = ('V' - 55), + }; + + enum e_Source { + SNone, SRiff, SIRiff, + }; + + enum { + fKeyOn = 1 << 0, + fKeyOff = 1 << 1, + fKeyedOn = 1 << 2, + }; + + struct CInstrument { + uint8_t Feedback[2]; + uint8_t Panning[2]; + uint8_t Algorithm; + uint8_t Detune; + uint8_t Volume; + uint8_t RiffSpeed; + uint8_t * Riff; + uint8_t Operators[4][5]; + }; + + struct CEffects { + int8_t PortSlide; + int8_t VolSlide; + uint16_t ToneSlideFreq; + uint8_t ToneSlideOct; + uint8_t ToneSlideSpeed; + int8_t ToneSlideDir; + }; + + struct CChannel { + uint8_t LastInstrument; + CInstrument * Instrument; + uint8_t Volume; + uint8_t DetuneA; + uint8_t DetuneB; + uint8_t KeyFlags; + uint16_t CurrFreq; + int8_t CurrOctave; + CEffects FX; + struct CRiff { + CEffects FX; + uint8_t * Track; + uint8_t * TrackStart; + uint8_t Line; + uint8_t Speed; + uint8_t SpeedCnt; + int8_t TransposeOctave; + int8_t TransposeNote; + uint8_t LastInstrument; + } Riff, IRiff; + }; + + public: + RADPlayer() : Initialised(false) {} + void Init(const void *tune, void (*opl3)(void *, uint16_t, uint8_t), void *arg); + void Stop(); + bool Update(); + int GetHertz() const { return Hertz; } + int GetPlayTimeInSeconds() const { return PlayTime / Hertz; } + int GetTunePos() const { return Order; } + int GetTuneLength() const { return OrderListSize; } + int GetTuneLine() const { return Line; } + void SetMasterVolume(int vol) { MasterVol = vol; } + int GetMasterVolume() const { return MasterVol; } + int GetSpeed() const { return Speed; } + +#if RAD_DETECT_REPEATS + uint32_t ComputeTotalTime(); +#endif + + private: + bool UnpackNote(uint8_t *&s, uint8_t &last_instrument); + uint8_t * GetTrack(); + uint8_t * SkipToLine(uint8_t *trk, uint8_t linenum, bool chan_riff = false); + void PlayLine(); + void PlayNote(int channum, int8_t notenum, int8_t octave, uint16_t instnum, uint8_t cmd = 0, uint8_t param = 0, e_Source src = SNone, int op = 0); + void LoadInstrumentOPL3(int channum); + void PlayNoteOPL3(int channum, int8_t octave, int8_t note); + void ResetFX(CEffects *fx); + void TickRiff(int channum, CChannel::CRiff &riff, bool chan_riff); + void ContinueFX(int channum, CEffects *fx); + void SetVolume(int channum, uint8_t vol); + void GetSlideDir(int channum, CEffects *fx); + void LoadInstMultiplierOPL3(int channum, int op, uint8_t mult); + void LoadInstVolumeOPL3(int channum, int op, uint8_t vol); + void LoadInstFeedbackOPL3(int channum, int which, uint8_t fb); + void Portamento(uint16_t channum, CEffects *fx, int8_t amount, bool toneslide); + void Transpose(int8_t note, int8_t octave); + void SetOPL3(uint16_t reg, uint8_t val) { + OPL3Regs[reg] = val; + OPL3(OPL3Arg, reg, val); + } + uint8_t GetOPL3(uint16_t reg) const { + return OPL3Regs[reg]; + } + + void (*OPL3)(void *, uint16_t, uint8_t); + void * OPL3Arg; + CInstrument Instruments[kInstruments]; + CChannel Channels[kChannels]; + uint32_t PlayTime; +#if RAD_DETECT_REPEATS + uint32_t OrderMap[4]; + bool Repeating; +#endif + int16_t Hertz; + uint8_t * OrderList; + uint8_t * Tracks[kTracks]; + uint8_t * Riffs[kRiffTracks][kChannels]; + uint8_t * Track; + bool Initialised; + uint8_t Speed; + uint8_t OrderListSize; + uint8_t SpeedCnt; + uint8_t Order; + uint8_t Line; + int8_t Entrances; + uint8_t MasterVol; + int8_t LineJump; + uint8_t OPL3Regs[512]; + + // Values exported by UnpackNote() + int8_t NoteNum; + int8_t OctaveNum; + uint8_t InstNum; + uint8_t EffectNum; + uint8_t Param; + bool LastNote; + + static const int8_t NoteSize[]; + static const uint16_t ChanOffsets3[9], Chn2Offsets3[9]; + static const uint16_t NoteFreq[]; + static const uint16_t OpOffsets3[9][4]; + static const bool AlgCarriers[7][4]; +}; + +#endif + diff --git a/converter.cpp b/converter.cpp new file mode 100644 index 0000000..19ed623 --- /dev/null +++ b/converter.cpp @@ -0,0 +1,53 @@ +#include + +#include "util/Bmp.h" +#include "util/Gbm.h" + +void usage() { + printf("Usage: gbmconv \n"); +} + +int main(int argc, char** argv) { + if (argc < 4) { + usage(); + return 1; + } + + char *gbmPath = argv[1]; + char *bmpPath = argv[2]; + char *maskBmpPath = argv[3]; + + auto bitmap = Bmp::loadFromFile(bmpPath); + if (bitmap.GetWidth() == 0 || bitmap.GetHeight() == 0) { + printf("Unable to load source bitmap\n"); + return 1; + } + + auto maskBitmap = Bmp::loadFromFile(maskBmpPath); + if (maskBitmap.GetWidth() == 0 || maskBitmap.GetHeight() == 0) { + printf("Unable to load mask bitmap\n"); + return 1; + } + + if (maskBitmap.GetWidth() != bitmap.GetWidth() || maskBitmap.GetHeight() != bitmap.GetHeight()) { + printf("Mask bitmap must be the same size as the source bitmap\n"); + return 1; + } + + // Create a combined bitmap where the most significant bit is a transparency bit + auto combined = Bitmap(bitmap.GetWidth(), bitmap.GetHeight()); + for (auto y = 0u; y < bitmap.GetHeight(); y++) { + for (auto x = 0u; x < bitmap.GetWidth(); x++) { + auto isTransparent = (maskBitmap.GetPixel(x, y) == 0); + auto pv = (isTransparent ? 0x80 : 0x00) | (bitmap.GetPixel(x, y) & 0x7f); + combined.SetPixel(x, y, pv); + } + } + for (auto i = 0u; i < 256; i++) { + combined.SetPaletteEntry(i, bitmap.GetPaletteEntry(i)); + } + + Gbm::writeToFile(gbmPath, combined); + printf("Wrote %s\n", gbmPath); + return 0; +} diff --git a/graphics/Bitmap.cpp b/graphics/Bitmap.cpp new file mode 100644 index 0000000..177d442 --- /dev/null +++ b/graphics/Bitmap.cpp @@ -0,0 +1,78 @@ +#include "Bitmap.h" + +#include +#include +#include + +Bitmap::~Bitmap() { + clear(); +} + +Bitmap::Bitmap(const Bitmap &other) { + copyFrom(other); +} + +Bitmap & Bitmap::operator=(const Bitmap &other) { + if (this != &other) { + clear(); + copyFrom(other); + } + + return *this; +} + +Bitmap::Bitmap(Bitmap &&other) noexcept { + moveFrom(std::move(other)); +} + +Bitmap & Bitmap::operator=(Bitmap &&other) noexcept { + moveFrom(std::move(other)); + + return *this; +} + +Bitmap::Bitmap(uint16_t width, uint16_t height) +: _width(width), _height(height), _data(new uint8_t[width * height]) { +} + +Bitmap::Bitmap(uint16_t width, uint16_t height, uint8_t *data) : Bitmap(width, height) { + memcpy(_data, data, width * height); +} + +void Bitmap::SetPaletteEntry(uint8_t index, Video::PaletteEntry entry) { + _pal[index] = entry; +} + +Video::PaletteEntry Bitmap::GetPaletteEntry(uint8_t index) const { + return _pal[index]; +} + +void Bitmap::clear() { + delete[] _data; + _data = nullptr; + _width = _height = 0; +} + +void Bitmap::copyFrom(const Bitmap &other) { + _width = other._width; + _height = other._height; + _data = new uint8_t[_width * _height]; + for (auto i = 0; i < 256; i++) _pal[i] = other._pal[i]; + + memcpy(_data, other._data, _width * _height); +} + +void Bitmap::moveFrom(Bitmap &&other) noexcept { + // Clear existing data + clear(); + + // Copy data from other + _data = other._data; + _width = other._width; + _height = other._height; + for (auto i = 0; i < 256; i++) _pal[i] = other._pal[i]; + + // Clear other + other._data = nullptr; + other._width = other._height = 0; +} diff --git a/graphics/Bitmap.h b/graphics/Bitmap.h new file mode 100644 index 0000000..3eacb8a --- /dev/null +++ b/graphics/Bitmap.h @@ -0,0 +1,66 @@ +#ifndef GAME_BITMAP_H +#define GAME_BITMAP_H + +#include + +#include "../system/Video.h" + +class Bitmap { +public: + Bitmap() = default; + + ~Bitmap(); + + Bitmap(const Bitmap& other); + Bitmap& operator=(const Bitmap& other); + + Bitmap(Bitmap &&other) noexcept ; + Bitmap& operator=(Bitmap &&other) noexcept; + + Bitmap(uint16_t width, uint16_t height); + + Bitmap(uint16_t width, uint16_t height, uint8_t* data); + + [[nodiscard]] uint8_t GetPixel(uint16_t x, uint16_t y) const { + return _data[y * _width + x]; + } + + void SetPixel(uint16_t x, uint16_t y, uint8_t value) { + _data[y * _width + x] = value; + } + + uint8_t &operator[](uint32_t offset) { + return _data[offset]; + } + + const uint8_t &operator[](uint32_t offset) const { + return _data[offset]; + } + + void SetPaletteEntry(uint8_t index, Video::PaletteEntry entry); + + [[nodiscard]] Video::PaletteEntry GetPaletteEntry(uint8_t index) const; + + [[nodiscard]] uint16_t GetWidth() const { + return _width; + } + + [[nodiscard]] uint16_t GetHeight() const { + return _height; + } + + void clear(); + +private: + uint16_t _width{0}; + uint16_t _height{0}; + uint8_t *_data{nullptr}; + + Video::PaletteEntry _pal[256]{}; + + void copyFrom(const Bitmap& other); + void moveFrom(Bitmap&& other) noexcept; +}; + + +#endif //GAME_BITMAP_H diff --git a/graphics/Font.h b/graphics/Font.h new file mode 100644 index 0000000..728c87d --- /dev/null +++ b/graphics/Font.h @@ -0,0 +1,67 @@ +#ifndef GAME_FONT_H +#define GAME_FONT_H + +#include "Bitmap.h" + +class Font { +public: + Font(Bitmap *bitmap, unsigned w, unsigned h, unsigned minCode = 32, unsigned maxCode = 127) + : _font(bitmap), _w(w), _h(h), _minCode(minCode), _maxCode(maxCode) { + _charsPerRow = _font->GetWidth() / _w; + } + + void renderCharAlpha(int c, int x, int y, uint8_t color, uint8_t *fb, unsigned fbWidth, unsigned fbHeight) { + if (c < _minCode || c > _maxCode) return; + c -= _minCode; + + auto bitmapX = (c % _charsPerRow) * _w; + auto bitmapY = (c / _charsPerRow) * _h; + + for (auto charY = 0; charY < _h; charY++) { + if (y + charY < 0) continue; + if (y + charY >= fbHeight) break; + auto yOffset = (y + charY) * fbWidth; + + for (auto charX = 0; charX < _w; charX++) { + if (x + charX < 0) continue; + if (x + charX >= fbWidth) break; + + auto pv = _font->GetPixel(bitmapX + charX, bitmapY + charY); + if (pv & 0x80) continue; + + fb[yOffset + x + charX] = color; + } + } + } + + void renderChar(int c, int x, int y, uint8_t fgColor, uint8_t bgColor, uint8_t *fb, unsigned fbWidth, unsigned fbHeight) { + if (c < _minCode || c > _maxCode) return; + c -= _minCode; + + auto bitmapX = (c % _charsPerRow) * _w; + auto bitmapY = (c / _charsPerRow) * _h; + + for (auto charY = 0; charY < _h; charY++) { + if (y + charY < 0) continue; + if (y + charY >= fbHeight) break; + auto yOffset = (y + charY) * fbWidth; + + for (auto charX = 0; charX < _w; charX++) { + if (x + charX < 0) continue; + if (x + charX >= fbWidth) break; + + auto pv = _font->GetPixel(bitmapX + charX, bitmapY + charY) & 0x80 ? bgColor : fgColor; + fb[yOffset + x + charX] = pv; + } + } + } +private: + Bitmap *_font; + unsigned _w, _h; + unsigned _minCode; + unsigned _maxCode; + unsigned _charsPerRow{0}; +}; + + +#endif //GAME_FONT_H \ No newline at end of file diff --git a/graphics/Rect.h b/graphics/Rect.h new file mode 100644 index 0000000..05e5a0a --- /dev/null +++ b/graphics/Rect.h @@ -0,0 +1,45 @@ +#ifndef GAME_RECT_H +#define GAME_RECT_H + +struct Rect { + int x1, y1, x2, y2; + + void clamp(int minX, int minY, int maxX, int maxY) { + if (x1 < minX) x1 = minX; + if (x2 > maxX) x2 = maxX; + if (y1 < minY) y1 = minY; + if (y2 > maxY) y2 = maxY; + } + + void sort() { + if (x1 > x2) { + auto x = x1; + x1 = x2; + x2 = x; + } + if (y1 > y2) { + auto y = y1; + y1 = y2; + y2 = y; + } + } + + Rect &operator+=(const Rect &other) { + if (other.x1 < x1) x1 = other.x1; + if (other.x2 > x2) x2 = other.x2; + if (other.y1 < y1) y1 = other.y1; + if (other.y2 > y2) y2 = other.y2; + + return *this; + } + + [[nodiscard]] bool contains(int x, int y) const { + return x >= x1 && y >= y1 && x < x2 && y < y2; + } + + [[nodiscard]] bool overlaps(const Rect &other) const { + return !(other.x1 > x2 || other.x2 < x1 || other.y1 > y2 || other.y2 < y1); + } +}; + +#endif //GAME_RECT_H \ No newline at end of file diff --git a/graphics/Sprite.h b/graphics/Sprite.h new file mode 100644 index 0000000..a98f0fe --- /dev/null +++ b/graphics/Sprite.h @@ -0,0 +1,68 @@ +#ifndef GAME_SPRITE_H +#define GAME_SPRITE_H +#include "Bitmap.h" + +#define SPR_SUBPX_TO_PX(v) ((v) >= 0 ? ((v) >> 8) : -((-(v)) >> 8)) +#define SPR_PX_TO_SUBPX(v) ((v) >= 0 ? ((v) << 8) : -((-(v)) << 8)) + +class Sprite { +public: + virtual ~Sprite() = default; + + virtual void Tick() = 0; + + [[nodiscard]] virtual int GetWidth() const { + return _w; + } + + [[nodiscard]] virtual int GetHeight() const { + return _h; + } + + [[nodiscard]] int GetX() const { + return SPR_SUBPX_TO_PX(_sx); + } + + [[nodiscard]] int GetY() const { + return SPR_SUBPX_TO_PX(_sy); + } + + void SetX(int x) { + _sx = x >= 0 ? (x << 8) : -((-x) << 8); + } + + void SetY(int y) { + _sy = y >= 0 ? (y << 8) : -((-y) << 8); + } + + [[nodiscard]] Rect GetRect() const { + auto x = SPR_SUBPX_TO_PX(_sx), y = SPR_SUBPX_TO_PX(_sy); + return { x, y, x + _w, y + _h }; + } + + void SetPalette(uint8_t palette) { _palette = palette << 4; } + + [[nodiscard]] uint8_t GetPalette() const { return _palette >> 4; } + + [[nodiscard]] Bitmap *GetBitmap() const { return _bitmap; } + + [[nodiscard]] unsigned GetYOffset() const { return _frame * _h; } + + [[nodiscard]] bool isMirroredX() const { return _mirrorX; } + + [[nodiscard]] bool isMirroredY() const { return _mirrorY; } + +protected: + Sprite() = default; + + Bitmap *_bitmap{nullptr}; + uint8_t _palette{0}; + int _sx{0}, _sy{0}; + int _w{0}, _h{0}; + unsigned _frame{0}; + bool _mirrorX{false}; + bool _mirrorY{false}; +}; + + +#endif //GAME_SPRITE_H \ No newline at end of file diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..4579009 --- /dev/null +++ b/install.bat @@ -0,0 +1,2 @@ +mkdir C:\game\ +xcopy *.* C:\game\ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..14aae1b --- /dev/null +++ b/main.cpp @@ -0,0 +1,111 @@ +#include +#include +#include + +#ifdef BUILD_SDL +#include +#endif + +#include "audio/AudioPlayer.h" +#include "audio/Music.h" +#include "system/Timer.h" +#include "system/Video.h" +#include "graphics/Bitmap.h" +#include "graphics/Font.h" + +#include "scenes/Scene.h" +#include "system/init.h" +#include "system/Keyboard.h" +#include "util/Gbm.h" +#include "util/Log.h" + +volatile bool showFps = true; +volatile bool running = true; + +int main(int argc, char *argv[]) { + System::init(); + + srand(time(nullptr)); + + DefaultLog.log("Loading music\n"); + Music music("spiral.rad"); + if (!music.isValid()) { + printf("Unable to load song\n"); + return 1; + } + + DefaultLog.log("Playing music\n"); + audioPlayer.playMusic(music); + + DefaultLog.log("Changing video mode\n"); + video.enter(); + + // Run scenes + auto fb = static_cast(video.GetFB()); + + unsigned fps{0}; + unsigned frameCount{0}; + uint32_t fpsTick{0}; + char fpsDigits[4]{'0', '0', '0', 0}; + auto fontBitmap = Gbm::loadFromFile("font1.gbm"); + Font font(&fontBitmap, 7, 9, 32, 127); + + keyboard.setKeyDownHandler([] (unsigned char key) { + DefaultLog.log("KeyDown %u (%s)\n", key, keyboard.getKeyName(key)); + }); + keyboard.setKeyUpHandler([] (unsigned char key) { + DefaultLog.log("KeyUp %u (%s)\n", key, keyboard.getKeyName(key)); + switch (key) { + case KeyEscape: + running = false; + break; + case KeyF: + showFps = !showFps; + video.UpdateRect({0, 0, 21, 9 }); + break; + default: + // Do nothing + break; + } + }); + keyboard.setKeyRepeatHandler([](unsigned char key) { + DefaultLog.log("KeyRepeat %u (%s)\n", key, keyboard.getKeyName(key)); + }); + + auto &timer = Timer::instance(); + while (running) { + const auto scene = Scenes::getCurrentScene(); + + scene->run(); + + // Render fps + ++frameCount; + auto ticks = timer.getTicks(); + if (ticks > fpsTick + 50) { + fpsTick = ticks; + fps = frameCount; + frameCount = 0; + fpsDigits[0] = '0' + (fps / 100) % 10; + fpsDigits[1] = '0' + (fps / 10) % 10; + fpsDigits[2] = '0' + fps % 10; + } + + if (showFps) { + font.renderChar(fpsDigits[0], 0, 0, 242, 0, fb, SCREEN_WIDTH, SCREEN_HEIGHT); + font.renderChar(fpsDigits[1], 7, 0, 242, 0, fb, SCREEN_WIDTH, SCREEN_HEIGHT); + font.renderChar(fpsDigits[2], 14, 0, 242, 0, fb, SCREEN_WIDTH, SCREEN_HEIGHT); + + video.UpdateRect({0, 0, 21, 9 }); + } + + // Wait for vertical retrace and then copy render buffer to screen + video.WaitForVerticalSync(); + video.Flip(); + } + + audioPlayer.stopMusic(); + + System::terminate(); + + return 0; +} diff --git a/scenes/GameScene.cpp b/scenes/GameScene.cpp new file mode 100644 index 0000000..052f75a --- /dev/null +++ b/scenes/GameScene.cpp @@ -0,0 +1,281 @@ +#include "GameScene.h" + +#include +#include + +#include "../system/Keyboard.h" +#include "../system/Timer.h" +#include "../system/Video.h" +#include "../util/Bmp.h" +#include "../util/Gbm.h" +#include "../util/Log.h" + +class TestSprite : public Sprite { +public: + enum { Width = 20, Height = 16, Frames = 12 }; + + explicit TestSprite(Bitmap *image) { + _bitmap = image; + _w = Width; + _h = Height; + SetX(10 + rand() % (SCREEN_WIDTH - 20 - Width)); + SetY(10 + rand() % (SCREEN_HEIGHT - 20 - Height)); + + int velXSign = ((rand() & 1) << 1) - 1; + int velYSign = ((rand() & 1) << 1) - 1; + _vx = (rand() % 128 + 64) * velXSign; + _vy = (rand() % 128 + 64) * velYSign; + } + + void Tick() override { + if (++_ticks >= 15) { + _ticks = 0; + if (++_frame >= Frames) { + _frame = 0; + } + } + + auto newX = _sx + _vx; + if (newX < 0) { + newX = -newX; + _vx = -_vx; + } else if (newX >= MaxX) { + auto d = newX - MaxX; + newX = MaxX - d - 1; + _vx = -_vx; + } + _mirrorX = (_vx < 0); + + auto newY = _sy + _vy; + if (newY < 0) { + newY = -newY; + _vy = -_vy; + } else if (newY >= MaxY) { + auto d = newY - MaxY; + newY = MaxY - d - 1; + _vy = -_vy; + } + + _sx = newX; + _sy = newY; + } + +protected: + int _vx{0}, _vy{0}; + unsigned _ticks{0}; + const int MaxX = (SCREEN_WIDTH - Width) << 8; + const int MaxY = (SCREEN_HEIGHT - Height) << 8; +}; + +class PlayerSprite : public Sprite { +public: + enum { Width = 24, Height = 32, Frames = 4, Speed = 480 }; + + explicit PlayerSprite(Bitmap *image) { + _bitmap = image; + _w = Width; + _h = Height; + SetX(20); + SetY((SCREEN_HEIGHT - Height) / 2); + } + + void Tick() override { + if (++_ticks >= 15) { + _ticks = 0; + if (++_animationStep >= Frames) { + _animationStep = 0; + } + + _frame = _animationMap[_animationStep]; + } + + if (keyboard.keyState[KeyLeft]) { + _sx -= Speed; + if (_sx < 0) _sx = 0; + } + if (keyboard.keyState[KeyRight]) { + _sx += Speed; + if (_sx > MaxX) _sx = MaxX; + } + if (keyboard.keyState[KeyUp]) { + _sy -= Speed; + if (_sy < 0) _sy = 0; + } + if (keyboard.keyState[KeyDown]) { + _sy += Speed; + if (_sy > MaxY) _sy = MaxY; + } + } + +protected: + unsigned _ticks{0}; + unsigned _animationStep = 0; + const int MaxX = (SCREEN_WIDTH - Width) << 8; + const int MaxY = (SCREEN_HEIGHT - Height) << 8; + const unsigned _animationMap[4] = { + 0, 1, 2, 1 + }; +}; + +GameScene::GameScene() { + +} + +GameScene::~GameScene() { +} + +void GameScene::run() { + if (_firstFrame) { + Rect bgRect{0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; + for (auto y = 0; y < SCREEN_HEIGHT; y++) { + auto yOffset = y * SCREEN_WIDTH; + for (auto x = 0; x < SCREEN_WIDTH; x++) { + _fb[yOffset + x] = _bg.GetPixel(x, y); + } + } + video.UpdateRect(bgRect); + _firstFrame = false; + } + + // Redraw background + for (auto &sprite : _sprites) { + clearSprite(sprite); + } + clearSprite(_playerSprite); + + // Update and paint all sprites after update + for (auto &sprite : _sprites) { + sprite->Tick(); + drawSprite(sprite); + } + + _playerSprite->Tick(); + drawSprite(_playerSprite); +} + +void GameScene::enter() { + DefaultLog.log("Entering GameScene\n"); + _fb = static_cast(video.GetFB()); + + _cow = Gbm::loadFromFile("cow.gbm"); + _witch = Gbm::loadFromFile("witch.gbm"); + _bg = Bmp::loadFromFile("bg.bmp"); + + _playerSprite = new PlayerSprite(&_witch); + _playerSprite->SetPalette(14); + + for (auto &sprite : _sprites) { + sprite = new TestSprite(&_cow); + sprite->SetPalette(15); + } + + // Load bg palette + for (auto i = 0u; i < 64; i++) { + video.SetPaletteEntry(i, _bg.GetPaletteEntry(i)); + } + + // Load sprite palette + for (auto i = 0u; i < 16; i++) { + video.SetPaletteEntry(i + 240, _cow.GetPaletteEntry(i)); + } + + // Load player sprite palette + for (auto i = 0u; i < 16; i++) { + video.SetPaletteEntry(i + 224, _witch.GetPaletteEntry(i)); + } + + _firstFrame = true; +} + +void GameScene::exit() { + DefaultLog.log("Exiting GameScene\n"); + + for (auto &sprite : _sprites) { + delete sprite; + } + + delete _playerSprite; + + _cow.clear(); + _bg.clear(); + _witch.clear(); +} + +void GameScene::clearSprite(Sprite *sprite) { + auto rect = sprite->GetRect(); + rect.clamp(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + + for (auto y = rect.y1; y < rect.y2; y++) { + auto yOffset = y * SCREEN_WIDTH; + + for (auto x = rect.x1; x < rect.x2; x++) { + _fb[yOffset + x] = _bg.GetPixel(x, y); + } + } + + video.UpdateRect(rect); +} + +void GameScene::drawSprite(Sprite *sprite) { + auto rect = sprite->GetRect(); + rect.clamp(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + + auto rx = rect.x1; + auto ry = rect.y1; + auto rx2 = rect.x2; + auto ry2 = rect.y2; + + auto paletteOffset = sprite->GetPalette() << 4; + auto bitmap = sprite->GetBitmap(); + auto syoff = sprite->GetYOffset(); + + if (sprite->isMirroredY()) { + for (auto y = ry; y < ry2; y++) { + auto yOffset = y * SCREEN_WIDTH; + auto offy = ry2 - y - 1; + + if (sprite->isMirroredX()) { + for (auto x = rx; x < rx2; x++) { + auto offx = rx2 - x - 1; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } else { + for (auto x = rx; x < rx2; x++) { + auto offx = x - rx; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } + } + } else { + for (auto y = ry; y < ry2; y++) { + auto yOffset = y * SCREEN_WIDTH; + auto offy = y -ry; + + if (sprite->isMirroredX()) { + for (auto x = rx; x < rx2; x++) { + auto offx = rx2 - x - 1; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } else { + for (auto x = rx; x < rx2; x++) { + auto offx = x - rx; + + auto pv = bitmap->GetPixel(offx, offy + syoff); + + if (!(pv & 0x80)) _fb[yOffset + x] = (pv & 0x7f) + paletteOffset; + } + } + } + } + + video.UpdateRect(rect); +} diff --git a/scenes/GameScene.h b/scenes/GameScene.h new file mode 100644 index 0000000..1bfd07f --- /dev/null +++ b/scenes/GameScene.h @@ -0,0 +1,37 @@ +#ifndef GAME_GAMESCENE_H +#define GAME_GAMESCENE_H + +#include + +#include "Scene.h" +#include "../graphics/Bitmap.h" +#include "../graphics/Sprite.h" + +#define MAX_OBJECTS 16 + +class GameScene : public Scene { + public: + GameScene(); + ~GameScene() override; + + void run() override; + + void enter() override; + + void exit() override; + private: + uint8_t *_fb{nullptr}; + + Bitmap _bg; + Bitmap _cow; + Bitmap _witch; + Sprite *_playerSprite; + Sprite *_sprites[MAX_OBJECTS]{nullptr}; + bool _firstFrame = true; + + void clearSprite(Sprite *sprite); + void drawSprite(Sprite *sprite); +}; + + +#endif //GAME_GAMESCENE_H \ No newline at end of file diff --git a/scenes/IntroScene.cpp b/scenes/IntroScene.cpp new file mode 100644 index 0000000..505b377 --- /dev/null +++ b/scenes/IntroScene.cpp @@ -0,0 +1,11 @@ +#include "IntroScene.h" + +void IntroScene::run() { + Scenes::setScene(Scenes::MainMenuScene); +} + +void IntroScene::enter() { +} + +void IntroScene::exit() { +} diff --git a/scenes/IntroScene.h b/scenes/IntroScene.h new file mode 100644 index 0000000..b6cfb79 --- /dev/null +++ b/scenes/IntroScene.h @@ -0,0 +1,20 @@ +#ifndef GAME_INTROSCENE_H +#define GAME_INTROSCENE_H + +#include "Scene.h" + + +class IntroScene : public Scene { +public: + IntroScene() = default; + ~IntroScene() override = default; + + void run() override; + + void enter() override; + + void exit() override; +}; + + +#endif //GAME_INTROSCENE_H \ No newline at end of file diff --git a/scenes/MainMenuScene.cpp b/scenes/MainMenuScene.cpp new file mode 100644 index 0000000..232c7c5 --- /dev/null +++ b/scenes/MainMenuScene.cpp @@ -0,0 +1,11 @@ +#include "MainMenuScene.h" + +void MainMenuScene::run() { + Scenes::setScene(Scenes::GameScene); +} + +void MainMenuScene::enter() { +} + +void MainMenuScene::exit() { +} diff --git a/scenes/MainMenuScene.h b/scenes/MainMenuScene.h new file mode 100644 index 0000000..d2a7482 --- /dev/null +++ b/scenes/MainMenuScene.h @@ -0,0 +1,20 @@ +#ifndef GAME_MAINMENUSCENE_H +#define GAME_MAINMENUSCENE_H + +#include "Scene.h" + + +class MainMenuScene : public Scene { +public: + MainMenuScene() = default; + ~MainMenuScene() override = default; + + void run() override; + + void enter() override; + + void exit() override; +}; + + +#endif //GAME_MAINMENUSCENE_H \ No newline at end of file diff --git a/scenes/Scene.cpp b/scenes/Scene.cpp new file mode 100644 index 0000000..2c82212 --- /dev/null +++ b/scenes/Scene.cpp @@ -0,0 +1,19 @@ +#include "Scene.h" + +#include "GameScene.h" +#include "IntroScene.h" +#include "MainMenuScene.h" + +namespace Scenes { + static ::IntroScene introScene{}; + static ::MainMenuScene mainMenuScene{}; + static ::GameScene gameScene{}; + + SceneId currentSceneId = IntroScene; + Scene *sceneList[] = { + &introScene, + &mainMenuScene, + &gameScene + }; +} + diff --git a/scenes/Scene.h b/scenes/Scene.h new file mode 100644 index 0000000..9b48582 --- /dev/null +++ b/scenes/Scene.h @@ -0,0 +1,42 @@ +#ifndef GAME_SCENE_H +#define GAME_SCENE_H + +#include "../graphics/Rect.h" + +class Scene { +public: + virtual ~Scene() = default; + virtual void run() = 0; + virtual void enter() = 0; + virtual void exit() = 0; + +protected: + Scene() = default; +}; + + +namespace Scenes { + enum SceneId { + IntroScene, + MainMenuScene, + GameScene, + }; + + extern SceneId currentSceneId; + extern Scene *sceneList[]; + + inline SceneId setScene(SceneId scene) { + auto previousSceneId = currentSceneId; + sceneList[currentSceneId]->exit(); + currentSceneId = scene; + sceneList[currentSceneId]->enter(); + return previousSceneId; + } + + inline Scene *getCurrentScene() { return sceneList[currentSceneId]; } + inline SceneId getCurrentSceneId() { return currentSceneId; } + +} + + +#endif //GAME_SCENE_H diff --git a/system/Keyboard.h b/system/Keyboard.h new file mode 100644 index 0000000..243db4c --- /dev/null +++ b/system/Keyboard.h @@ -0,0 +1,5 @@ +#ifdef BUILD_SDL +#include "sdl/Keyboard.h" +#else +#include "dos/Keyboard.h" +#endif \ No newline at end of file diff --git a/system/Opl.h b/system/Opl.h new file mode 100644 index 0000000..638b42e --- /dev/null +++ b/system/Opl.h @@ -0,0 +1,10 @@ +#ifndef GAME_OPL_H +#define GAME_OPL_H + +#include + +namespace Opl { + void write(uint16_t reg, uint8_t data); +} + +#endif //GAME_OPL_H \ No newline at end of file diff --git a/system/Pic.h b/system/Pic.h new file mode 100644 index 0000000..89891fa --- /dev/null +++ b/system/Pic.h @@ -0,0 +1,12 @@ +#ifndef GAME_PIC_H +#define GAME_PIC_H + +#include "../util/Asm.h" + +namespace Pic { + static void clearInterrupt() { + outb(0x20, 0x20); + } +} + +#endif //GAME_PIC_H \ No newline at end of file diff --git a/system/SoundBlaster.h b/system/SoundBlaster.h new file mode 100644 index 0000000..55d533c --- /dev/null +++ b/system/SoundBlaster.h @@ -0,0 +1,37 @@ +#ifndef GAME_SOUNDBLASTER_H +#define GAME_SOUNDBLASTER_H + +#ifdef BUILD_SDL +#include +#else +#include +#endif +#include + +#define SB_BUFFER_SIZE 1024 +#define SB_BUFFERS 32 +#define SB_DMA_BUFFER_SIZE (SB_BUFFER_SIZE * 4) + +class SoundBlaster { +public: + SoundBlaster(); + ~SoundBlaster(); + +private: + friend void soundblasterIsr(); + void onInterrupt(); + +#ifdef BUILD_SDL +#else + _go32_dpmi_seginfo _dmaBuffer{}; + uint8_t (*_buffers)[SB_BUFFER_SIZE]{}; + uint8_t _dmaPage{0}; + uint16_t _dmaOffset{0}; + unsigned _nextBufferReadIndex{0}; + unsigned _nextBufferWriteIndex{0}; +#endif +}; + +extern SoundBlaster soundblaster; + +#endif //GAME_SOUNDBLASTER_H diff --git a/system/Timer.h b/system/Timer.h new file mode 100644 index 0000000..f75cf70 --- /dev/null +++ b/system/Timer.h @@ -0,0 +1,49 @@ +#ifndef TICKS_H +#define TICKS_H + +#include + +#ifdef BUILD_SDL +#else +#include +#endif + +class Timer { +public: + using Callback = void (*)(); + + Timer(); + + ~Timer(); + + void setFrequency(uint16_t freq); + + void setDivider(uint16_t div); + + Callback setCallback(Callback); + + [[nodiscard]] uint32_t getTicks() const; + + [[nodiscard]] uint16_t getFrequency() const; + + static Timer &instance(); + +private: + friend void timerISR(); + +#ifdef BUILD_SDL +#else + _go32_dpmi_seginfo _oldIsr{}, _newIsr{}; +#endif + + uint32_t _ticks{0}; + uint32_t _elapsed{0}; + uint16_t _div{0}; + uint16_t _freq{0}; + + void (*_callback)(){nullptr}; + + void update(); +}; + +#endif diff --git a/system/Video.h b/system/Video.h new file mode 100644 index 0000000..e7b621f --- /dev/null +++ b/system/Video.h @@ -0,0 +1,63 @@ +#ifndef VIDEO_H +#define VIDEO_H + +#ifdef BUILD_SDL +#include +#include +#else +#include +#endif + +#include "../graphics/Rect.h" + +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 200 +#define MAX_UPDATE_RECT_INDEX 255 + +class Video { + public: + struct PaletteEntry { + uint8_t r{0}, g{0}, b{0}; + }; + + Video(); + ~Video(); + + void enter(); + void exit(); + + void SetPaletteEntry(uint8_t index, PaletteEntry entry); + PaletteEntry GetPaletteEntry(uint8_t index); + + void SetPaletteEntry(uint8_t index, PaletteEntry *entry); + void GetPaletteEntry(uint8_t index, PaletteEntry *entry); + + void *GetFB(); + + void WaitForVerticalSync(); + + void UpdateRect(const Rect &rect); + void Flip(); + private: + uint8_t _oldMode{0}; + PaletteEntry _oldPalette[256]{}; +#ifdef BUILD_SDL + SDL_Window *_window; + SDL_Surface *_windowSurface; + SDL_Surface *_fb; + PaletteEntry _palette[256]{}; + std::chrono::steady_clock::time_point _lastUpdate{std::chrono::steady_clock::now()}; +#else + uint8_t *_fb{nullptr}; +#endif + uint8_t _renderBuffer[SCREEN_WIDTH*SCREEN_HEIGHT]{}; + Rect _updatedRects[MAX_UPDATE_RECT_INDEX+1]; + unsigned _updateRectIndex{0}; + + void SetMode(uint8_t mode); + uint8_t GetMode(); +}; + +extern Video video; + +#endif diff --git a/system/dos/Keyboard.cpp b/system/dos/Keyboard.cpp new file mode 100644 index 0000000..50f2972 --- /dev/null +++ b/system/dos/Keyboard.cpp @@ -0,0 +1,429 @@ +#include "../Keyboard.h" + +#include + +#include "../Pic.h" + +#define PIC +#define KEYB_DATA 0x60 + +Keyboard keyboard; + +static const char *keyNames[] = { + "", + "KeyEscape", // = 0x01, + "Key1", // = 0x02, + "Key2", // = 0x03, + "Key3", // = 0x04, + "Key4", // = 0x05, + "Key5", // = 0x06, + "Key6", // = 0x07, + "Key7", // = 0x08, + "Key8", // = 0x09, + "Key9", // = 0x0a, + "Key0", // = 0x0b, + "KeyMinus", // = 0x0c, + "KeyEqual", // = 0x0d, + "KeyBackspace", // = 0x0e, + "KeyTab", // = 0x0f, + + "KeyQ", // = 0x10, + "KeyW", // = 0x11, + "KeyE", // = 0x12, + "KeyR", // = 0x13, + "KeyT", // = 0x14, + "KeyY", // = 0x15, + "KeyU", // = 0x16, + "KeyI", // = 0x17, + "KeyO", // = 0x18, + "KeyP", // = 0x19, + "KeyBracketLeft", // = 0x1a, + "KeyBracketRight", // = 0x1b, + "KeyEnter", // = 0x1c, + "KeyLeftCtrl", // = 0x1d, + "KeyA", // = 0x1e, + "KeyS", // = 0x1f, + + "KeyD", // = 0x20, + "KeyF", // = 0x21, + "KeyG", // = 0x22, + "KeyH", // = 0x23, + "KeyJ", // = 0x24, + "KeyK", // = 0x25, + "KeyL", // = 0x26, + "KeySemicolon", // = 0x27, + "KeyApostrophe", // = 0x28, + "KeyBacktick", // = 0x29, + "KeyLeftShift", // = 0x2a, + "KeyBackslash", // = 0x2b, + "KeyZ", // = 0x2c, + "KeyX", // = 0x2d, + "KeyC", // = 0x2e, + "KeyV", // = 0x2f, + + "KeyB", // = 0x30, + "KeyN", // = 0x31, + "KeyM", // = 0x32, + "KeyComma", // = 0x33, + "KeyPeriod", // = 0x34, + "KeySlash", // = 0x35, + "KeyRightShift", // = 0x36, + "KeyKeypadMultiply", // = 0x37, + "KeyLeftAlt", // = 0x38, + "KeySpace", // = 0x39, + "KeyCapsLock", // = 0x3a, + "KeyF1", // = 0x3b, + "KeyF2", // = 0x3c, + "KeyF3", // = 0x3d, + "KeyF4", // = 0x3e, + "KeyF5", // = 0x3f, + + "KeyF6", // = 0x40, + "KeyF7", // = 0x41, + "KeyF8", // = 0x42, + "KeyF9", // = 0x43, + "KeyF10", // = 0x44, + "KeyNumLock", // = 0x45, + "KeyScrollLock", // = 0x46, + "KeyKeypad7", // = 0x47, + "KeyKeypad8", // = 0x48, + "KeyKeypad9", // = 0x49, + "KeyKeypadMinus", // = 0x4a, + "KeyKeypad4", // = 0x4b, + "KeyKeypad5", // = 0x4c, + "KeyKeypad6", // = 0x4d, + "KeyKeypadPlus", // = 0x4e, + "KeyKeypad1", // = 0x4f, + + "KeyKeypad2", // = 0x50, + "KeyKeypad3", // = 0x51, + "KeyKeypad0", // = 0x52, + "KeyKeypadPeriod", // = 0x53, + "", + "", + "", + "KeyF11", // = 0x57, + "KeyF12", // = 0x58, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "KeyMediaPrev", // = 0x90, + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaNext", // = 0x99, + "", + "", + "KeyKeypadEnter", // = 0x9c, + "KeyRightControl", // = 0x9d, + "", + "", + + "KeyMediaMute", // = 0xa0, + "KeyMediaCalculator", // = 0xa1, + "KeyMediaPlay", // = 0xa2, + "", + "KeyMediaStop", // = 0xa4, + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaVolumeDown", // = 0xae, + "", + + "KeyMediaVolumeUp", // = 0xb0, + "", + "KeyMediaWww", // = 0xb2, + "", + "", + "KeyKeypadDivide", // = 0xb5, + "", + "", + "KeyRightAlt", // = 0xb8, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "KeyHome", // = 0xc7, + "KeyUp", // = 0xc8, + "KeyPageUp", // = 0xc9, + "", + "KeyLeft", // = 0xcb, + "", + "KeyRight", // = 0xcd, + "", + "KeyEnd", // = 0xcf, + + "KeyDown", // = 0xd0, + "KeyPageDown", // = 0xd1, + "KeyInsert", // = 0xd2, + "KeyDelete", // = 0xd3, + "", + "", + "", + "", + "", + "", + "", + "KeyLeftGui", // = 0xdb, + "KeyRightGui", // = 0xdc, + "KeyApps", // = 0xdd, + "KeyAcpiPower", // = 0xde, + "KeyAcpiSleep", // = 0xdf, + + "", + "", + "", + "KeyAcpiWake", // = 0xe3, + "", + "KeyMediaWwwSearch", // = 0xe5, + "KeyMediaWwwFavorites", // = 0xe6, + "KeyMediaWwwRefresh", // = 0xe7, + "KeyMediaWwwStop", // = 0xe8, + "KeyMediaWwwForward", // = 0xe9, + "KeyMediaWwwBack", // = 0xea, + "KeyMediaMyComputer", // = 0xeb, + "KeyMediaEmail", // = 0xec, + "KeyMediaSelect", // = 0xed, + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyPrint", // = 0xfe, + "KeyPause", // = 0xff, +}; + +void keyboardIsr() { + keyboard.Isr(); +} + +Keyboard::Keyboard() { + _go32_dpmi_get_protected_mode_interrupt_vector(9, &_oldIsr); + + _newIsr.pm_offset = (int)keyboardIsr; + _newIsr.pm_selector = _go32_my_cs(); + _go32_dpmi_allocate_iret_wrapper(&_newIsr); + _go32_dpmi_set_protected_mode_interrupt_vector(9, &_newIsr); +} + +Keyboard::~Keyboard() { + _go32_dpmi_set_protected_mode_interrupt_vector(9, &_oldIsr); + _go32_dpmi_free_iret_wrapper(&_newIsr); +} + +Keyboard::KeyHandleFunction Keyboard::setKeyUpHandler(KeyHandleFunction handler) { + auto oldHandler = _keyUpHandler; + _keyUpHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyDownHandler(KeyHandleFunction handler) { + auto oldHandler = _keyDownHandler; + _keyDownHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyRepeatHandler(KeyHandleFunction handler) { + auto oldHandler = _keyRepeatHandler; + _keyRepeatHandler = handler; + return oldHandler; +} + +const char *Keyboard::getKeyName(unsigned char keyCode) { + return keyNames[keyCode]; +} + +void Keyboard::Isr() { + auto code = inb(KEYB_DATA); + switch (_state) { + case Start: + if (code == 0xe0) { + _state = Extended; + break; + } + if (code == 0xe1) { + _state = PauseBegin; + break; + } + if (code & 0x80) { + _state = Start; + keyUp(code & 0x7f); + } else { + _state = Start; + keyDown(code & 0x7f); + } + break; + case Extended: + if (code == 0x2a) { + _state = PrintPressed1; + break; + } + if (code == 0xb7) { + _state = PrintReleased1; + break; + } + if (code & 0x80) { + _state = Start; + keyUp(0x80 + (code & 0x7f)); + } else { + _state = Start; + keyDown(0x80 + code); + } + break; + case PauseBegin: + if (code == 0x1d) { + _state = PausePressed1; + break; + } + if (code == 0x9d) { + _state = PauseReleased1; + break; + } + _state = Start; + break; + case PausePressed1: + if (code == 45) { + keyDown(KeyPause); + } + _state = Start; + break; + case PauseReleased1: + if (code == 0xc5) { + keyUp(KeyPause); + } + _state = Start; + break; + case PrintPressed1: + if (code == 0xe0) { + _state = PrintPressed2; + break; + } + _state = Start; + break; + case PrintPressed2: + if (code == 0x37) { + keyDown(KeyPrint); + } + _state = Start; + break; + case PrintReleased1: + if (code == 0xe0) { + _state = PrintReleased2; + break; + } + _state = Start; + break; + case PrintReleased2: + if (code == 0xaa) { + keyUp(KeyPrint); + } + _state = Start; + break; + } + + Pic::clearInterrupt(); +} + +void Keyboard::keyDown(unsigned char c) { + if (keyState[c]) { + if (_keyRepeatHandler) _keyRepeatHandler(c); + } else { + keyState[c] = true; + if (_keyDownHandler) _keyDownHandler(c); + } +} + +void Keyboard::keyUp(unsigned char c) { + keyState[c] = false; + if (_keyUpHandler) _keyUpHandler(c); +} diff --git a/system/dos/Keyboard.h b/system/dos/Keyboard.h new file mode 100644 index 0000000..1e3b729 --- /dev/null +++ b/system/dos/Keyboard.h @@ -0,0 +1,182 @@ +#ifndef GAME_KEYBOARD_H +#define GAME_KEYBOARD_H + +#ifdef BUILD_SDL +#else +#include +#endif + +#define KEY_MAX 256 + +enum { + KeyEscape = 0x01, + Key1 = 0x02, + Key2 = 0x03, + Key3 = 0x04, + Key4 = 0x05, + Key5 = 0x06, + Key6 = 0x07, + Key7 = 0x08, + Key8 = 0x09, + Key9 = 0x0a, + Key0 = 0x0b, + KeyMinus = 0x0c, + KeyEqual = 0x0d, + KeyBackspace = 0x0e, + KeyTab = 0x0f, + KeyQ = 0x10, + KeyW = 0x11, + KeyE = 0x12, + KeyR = 0x13, + KeyT = 0x14, + KeyY = 0x15, + KeyU = 0x16, + KeyI = 0x17, + KeyO = 0x18, + KeyP = 0x19, + KeyBracketLeft = 0x1a, + KeyBracketRight = 0x1b, + KeyEnter = 0x1c, + KeyLeftCtrl = 0x1d, + KeyA = 0x1e, + KeyS = 0x1f, + KeyD = 0x20, + KeyF = 0x21, + KeyG = 0x22, + KeyH = 0x23, + KeyJ = 0x24, + KeyK = 0x25, + KeyL = 0x26, + KeySemicolon = 0x27, + KeyApostrophe = 0x28, + KeyBacktick = 0x29, + KeyLeftShift = 0x2a, + KeyBackslash = 0x2b, + KeyZ = 0x2c, + KeyX = 0x2d, + KeyC = 0x2e, + KeyV = 0x2f, + KeyB = 0x30, + KeyN = 0x31, + KeyM = 0x32, + KeyComma = 0x33, + KeyPeriod = 0x34, + KeySlash = 0x35, + KeyRightShift = 0x36, + KeyKeypadMultiply = 0x37, + KeyLeftAlt = 0x38, + KeySpace = 0x39, + KeyCapsLock = 0x3a, + KeyF1 = 0x3b, + KeyF2 = 0x3c, + KeyF3 = 0x3d, + KeyF4 = 0x3e, + KeyF5 = 0x3f, + KeyF6 = 0x40, + KeyF7 = 0x41, + KeyF8 = 0x42, + KeyF9 = 0x43, + KeyF10 = 0x44, + KeyNumLock = 0x45, + KeyScrollLock = 0x46, + KeyKeypad7 = 0x47, + KeyKeypad8 = 0x48, + KeyKeypad9 = 0x49, + KeyKeypadMinus = 0x4a, + KeyKeypad4 = 0x4b, + KeyKeypad5 = 0x4c, + KeyKeypad6 = 0x4d, + KeyKeypadPlus = 0x4e, + KeyKeypad1 = 0x4f, + KeyKeypad2 = 0x50, + KeyKeypad3 = 0x51, + KeyKeypad0 = 0x52, + KeyKeypadPeriod = 0x53, + KeyF11 = 0x57, + KeyF12 = 0x58, + KeyMediaPrev = 0x90, + KeyMediaNext = 0x99, + KeyKeypadEnter = 0x9c, + KeyRightControl = 0x9d, + KeyMediaMute = 0xa0, + KeyMediaCalculator = 0xa1, + KeyMediaPlay = 0xa2, + KeyMediaStop = 0xa4, + KeyMediaVolumeDown = 0xae, + KeyMediaVolumeUp = 0xb0, + KeyMediaWww = 0xb2, + KeyKeypadDivide = 0xb5, + KeyRightAlt = 0xb8, + KeyHome = 0xc7, + KeyUp = 0xc8, + KeyPageUp = 0xc9, + KeyLeft = 0xcb, + KeyRight = 0xcd, + KeyEnd = 0xcf, + KeyDown = 0xd0, + KeyPageDown = 0xd1, + KeyInsert = 0xd2, + KeyDelete = 0xd3, + KeyLeftGui = 0xdb, + KeyRightGui = 0xdc, + KeyApps = 0xdd, + KeyAcpiPower = 0xde, + KeyAcpiSleep = 0xdf, + KeyAcpiWake = 0xe3, + KeyMediaWwwSearch = 0xe5, + KeyMediaWwwFavorites = 0xe6, + KeyMediaWwwRefresh = 0xe7, + KeyMediaWwwStop = 0xe8, + KeyMediaWwwForward = 0xe9, + KeyMediaWwwBack = 0xea, + KeyMediaMyComputer = 0xeb, + KeyMediaEmail = 0xec, + KeyMediaSelect = 0xed, + KeyPrint = 0xfe, + KeyPause = 0xff, +}; + +class Keyboard { +public: + using KeyHandleFunction = void (*)(unsigned char); + + Keyboard(); + ~Keyboard(); + + KeyHandleFunction setKeyUpHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyDownHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyRepeatHandler(KeyHandleFunction handler); + + bool keyState[KEY_MAX]{}; + + const char *getKeyName(unsigned char keyCode); +private: + friend void keyboardIsr(); + void Isr(); + enum State { + Start, + Extended, + PauseBegin, + PausePressed1, + PauseReleased1, + PrintPressed1, + PrintPressed2, + PrintReleased1, + PrintReleased2, + }; + State _state; + KeyHandleFunction _keyDownHandler{nullptr}, _keyUpHandler{nullptr}, _keyRepeatHandler{nullptr}; + + void keyDown(unsigned char c); + + void keyUp(unsigned char c); + +#ifdef BUILD_SDL +#else + _go32_dpmi_seginfo _oldIsr, _newIsr; +#endif +}; + +extern Keyboard keyboard; + +#endif //GAME_KEYBOARD_H diff --git a/system/dos/Opl.cpp b/system/dos/Opl.cpp new file mode 100644 index 0000000..5d6794b --- /dev/null +++ b/system/dos/Opl.cpp @@ -0,0 +1,18 @@ +#include "../Opl.h" +#include "../../util/Asm.h" +#include "../../util/Log.h" + +#define OPL_REG 0x388 + +namespace Opl { + void write(uint16_t reg, uint8_t data) { + DefaultLog.log("Writing to OPL r=%u data=%u\n", reg, data); + if (reg >= 0x100) { + outb(OPL_REG + 2, reg & 0xff); + outb(OPL_REG + 3, data); + } else { + outb(OPL_REG, reg); + outb(OPL_REG + 1, data); + } + } +} diff --git a/system/dos/SoundBlaster.cpp b/system/dos/SoundBlaster.cpp new file mode 100644 index 0000000..fe2dc15 --- /dev/null +++ b/system/dos/SoundBlaster.cpp @@ -0,0 +1,47 @@ +#include "../SoundBlaster.h" + +#include + +SoundBlaster soundblaster; + +void soundblasterIsr() { + soundblaster.onInterrupt(); +} + +SoundBlaster::SoundBlaster() { + // Allocate twice the amount needed and select one half depending on whether it crosses a 64k boundary + _dmaBuffer.size = (SB_DMA_BUFFER_SIZE + 15) >> (4-1); + _go32_dpmi_allocate_dos_memory(&_dmaBuffer); + + auto physFirst = (_dmaBuffer.rm_segment << 4) + _dmaBuffer.rm_offset; + auto physSecond = physFirst + SB_DMA_BUFFER_SIZE; + auto pageFirst = physFirst >> 16; + auto pageSecond = physSecond >> 16; + + if (pageFirst == pageSecond) { + // Use first half + _buffers = reinterpret_cast(physFirst); + _dmaPage = pageFirst; + _dmaOffset = physFirst & 0xFFFF; + } else { + // Use second half + _buffers = reinterpret_cast(physSecond); + _dmaPage = pageSecond; + _dmaOffset = physSecond & 0xFFFF; + } +} + +SoundBlaster::~SoundBlaster() { + _go32_dpmi_free_dos_memory(&_dmaBuffer); +} + +void SoundBlaster::onInterrupt() { + // No data available - should not happen, but let's force generate a block + if (_nextBufferReadIndex == _nextBufferWriteIndex) { + // TODO generateSamples(); + } + + dosmemput(_buffers, SB_BUFFER_SIZE, (_dmaBuffer.rm_segment << 4) + _nextBufferReadIndex * SB_BUFFER_SIZE); + _nextBufferReadIndex = (_nextBufferReadIndex + 1) & 3; +} + diff --git a/system/dos/Timer.cpp b/system/dos/Timer.cpp new file mode 100644 index 0000000..7882022 --- /dev/null +++ b/system/dos/Timer.cpp @@ -0,0 +1,106 @@ +#include + +#include "../Timer.h" + +#include "../Pic.h" +#include "../../util/Asm.h" +#include "../../util/Log.h" + +#define PIT_FREQ 1193181 +#define PIT_MODE_CH0 0x00 +#define PIT_MODE_CH1 0x40 +#define PIT_MODE_CH2 0x80 +#define PIT_MODE_ACCESS_LO 0x10 +#define PIT_MODE_ACCESS_HI 0x20 +#define PIT_MODE_ACCESS_LOHI 0x30 +#define PIT_MODE_M0 0x00 +#define PIT_MODE_M1 0x02 +#define PIT_MODE_M2 0x04 +#define PIT_MODE_M3 0x06 +#define PIT_MODE_M4 0x08 +#define PIT_MODE_M5 0x0a +#define PIT_MODE_BIN 0x00 +#define PIT_MODE_BCD 0x01 + +#define PIT_REG_CH0 0x40 +#define PIT_REG_CH1 0x41 +#define PIT_REG_CH2 0x42 +#define PIT_REG_MODE 0x43 + +void timerISR() { + Timer::instance().update(); +} + +Timer::Timer() { + _go32_dpmi_get_protected_mode_interrupt_vector(8, &_oldIsr); + + _newIsr.pm_offset = reinterpret_cast(timerISR); + _newIsr.pm_selector = _go32_my_cs(); + _go32_dpmi_allocate_iret_wrapper(&_newIsr); + _go32_dpmi_set_protected_mode_interrupt_vector(8, &_newIsr); +} + +Timer::~Timer() { + setDivider(65535); + _go32_dpmi_set_protected_mode_interrupt_vector(8, &_oldIsr); + _go32_dpmi_free_iret_wrapper(&_newIsr); +} + +void Timer::update() { + _ticks++; + + if (_callback) { + DefaultLog.log("Timer tick with callback\n"); + _callback(); + } else { + DefaultLog.log("Timer tick without callback\n"); + } + + _elapsed += _div; + if (_elapsed >= 65535) { + _elapsed -= 65535; + // TODO call old isr + // Documentation is kind of nonexistant so we will not call the handler for now + Pic::clearInterrupt(); + } else { + Pic::clearInterrupt(); + } +} + +uint32_t Timer::getTicks() const { + return _ticks; +} + +uint16_t Timer::getFrequency() const { + return _freq; +} + +Timer &Timer::instance() { + static Timer inst; + return inst; +} + +void Timer::setFrequency(uint16_t freq) { + _freq = freq; + setDivider(PIT_FREQ / freq); +} + +void Timer::setDivider(uint16_t div) { + auto wasEnabled = interruptsEnabled(); + disableInterrupts(); + + _div = div; + + // Update PIT frequency + outb(PIT_REG_MODE, PIT_MODE_CH0|PIT_MODE_ACCESS_LOHI|PIT_MODE_M2|PIT_MODE_BIN); + outb(PIT_REG_CH0, _div & 0xff); + outb(PIT_REG_CH0, (_div >> 8) & 0xff); + + if (wasEnabled) enableInterrupts(); +} + +Timer::Callback Timer::setCallback(Timer::Callback callback) { + auto oldCallback = _callback; + _callback = callback; + return oldCallback; +} diff --git a/system/dos/Video.cpp b/system/dos/Video.cpp new file mode 100644 index 0000000..bf1dc57 --- /dev/null +++ b/system/dos/Video.cpp @@ -0,0 +1,135 @@ +#include +#include + +#include "../Video.h" +#include "../../util/Asm.h" +#include "../../util/Log.h" + +#define VGA_DAC_ADDR_RD 0x3c7 +#define VGA_DAC_ADDR_WR 0x3c8 +#define VGA_DAC_DATA 0x3c9 +#define VGA_EXT_INPUT_STATUS 0x3da +#define VGA_EXT_INPUT_STATUS_VRETRACE 8 + +Video video; + +Video::Video() { + __djgpp_nearptr_enable(); + + // Store current video mode + _oldMode = GetMode(); + + // Store current palette + for (auto i = 0; i < 256; i++) { + GetPaletteEntry(i, &_oldPalette[i]); + } +} + +Video::~Video() { + exit(); +} + +void Video::enter() { + SetMode(0x13); +} + +void Video::exit() { + // Restore old palette + for (auto i = 0; i < 256; i++) { + SetPaletteEntry(i, &_oldPalette[i]); + } + + // Restore old video mode + SetMode(_oldMode); +} + +void Video::SetMode(uint8_t mode) { + union REGS regs; + regs.h.ah = 0x00; + regs.h.al = mode; + int86(0x10, ®s, ®s); + + //__djgpp_nearptr_enable(); + switch (mode) { + default: + case 0x13: + _fb = (uint8_t *)(0xA0000 + __djgpp_conventional_base); + + } +} + +uint8_t Video::GetMode() { + union REGS regs; + regs.h.ah = 0x0f; + regs.h.al = 0; + int86(0x10, ®s, ®s); + + return regs.h.al; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry entry) { + SetPaletteEntry(index, &entry); +} + +Video::PaletteEntry Video::GetPaletteEntry(uint8_t index) { + PaletteEntry entry; + GetPaletteEntry(index, &entry); + return entry; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry *entry) { + outb(VGA_DAC_ADDR_WR, index); + outb(VGA_DAC_DATA, entry->r >> 2); + outb(VGA_DAC_DATA, entry->g >> 2); + outb(VGA_DAC_DATA, entry->b >> 2); +} + +void Video::GetPaletteEntry(uint8_t index, PaletteEntry *entry) { + outb(VGA_DAC_ADDR_RD, index); + entry->r = inb(VGA_DAC_DATA); + entry->g = inb(VGA_DAC_DATA); + entry->b = inb(VGA_DAC_DATA); +} + +void *Video::GetFB() { + return _renderBuffer; +} + +void Video::WaitForVerticalSync() { + auto state = inb(VGA_EXT_INPUT_STATUS) & VGA_EXT_INPUT_STATUS_VRETRACE; + if (!state) { + // We started before a retrace, so wait until it begins + while (!(inb(VGA_EXT_INPUT_STATUS) & VGA_EXT_INPUT_STATUS_VRETRACE)) {} + } + // Wait until it ends + while (inb(VGA_EXT_INPUT_STATUS) & VGA_EXT_INPUT_STATUS_VRETRACE); +} + +void Video::UpdateRect(const Rect &rect) { + // Merge rect if it overlaps with an existing rect + for (auto i = 0; i < _updateRectIndex; i++) { + auto &r = _updatedRects[i]; + if (r.overlaps(rect)) { + r += rect; + return; + } + } + + // Add a new rect to the list + _updatedRects[_updateRectIndex++] = rect; +} + +void Video::Flip() { + for (auto i = 0; i <= MAX_UPDATE_RECT_INDEX; i++) { + auto &rect = _updatedRects[i]; + for (auto y = rect.y1; y <= rect.y2; y++) { + auto yOffset = y * SCREEN_WIDTH; + + for (auto x = rect.x1; x <= rect.x2; x++) { + _fb[yOffset + x] = _renderBuffer[yOffset + x]; + } + } + } + + _updateRectIndex = 0; +} diff --git a/system/dos/init.cpp b/system/dos/init.cpp new file mode 100644 index 0000000..a4d86fc --- /dev/null +++ b/system/dos/init.cpp @@ -0,0 +1,12 @@ +#include "../init.h" + +namespace System { + void init() { + + } + + void terminate() { + + } +} + diff --git a/system/init.h b/system/init.h new file mode 100644 index 0000000..045f0b1 --- /dev/null +++ b/system/init.h @@ -0,0 +1,9 @@ +#ifndef GAME_INIT_H +#define GAME_INIT_H + +namespace System { + void init(); + void terminate(); +} + +#endif //GAME_INIT_H \ No newline at end of file diff --git a/system/sdl/AudioBackend.cpp b/system/sdl/AudioBackend.cpp new file mode 100644 index 0000000..067e627 --- /dev/null +++ b/system/sdl/AudioBackend.cpp @@ -0,0 +1,52 @@ +#include "AudioBackend.h" + +#include + +AudioBackend audioBackend; + +AudioBackend::AudioBackend() { +} + +AudioBackend::~AudioBackend() { + terminate(); +} + +void AudioBackend::init() { + SDL_AudioSpec desired; + desired.freq = 44100; + desired.format = AUDIO_S16LSB; + desired.channels = 2; + desired.samples = 1024; + desired.padding = 0; + desired.userdata = this; + desired.callback = [] (void *userdata, Uint8 *stream, int len) { + static_cast(userdata)->generate(stream, len); + }; + + _audioDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &_audioSpec, 0); + if (!_audioDevice) { + std::cerr << "Failed to open audio device: " << SDL_GetError() << std::endl; + abort(); + } + + SDL_PauseAudioDevice(_audioDevice, 0); +} + +void AudioBackend::terminate() { + if (_audioDevice) { + SDL_PauseAudioDevice(_audioDevice, 1); + SDL_CloseAudioDevice(_audioDevice); + } +} + +void AudioBackend::generate(Uint8 *stream, int len) { + len = len / 4; + auto s = reinterpret_cast(stream); + + for (int i = 0; i < len; i++) { + int16_t l, r; + Opl::generateSample(&l, &r); + *s++ = l; + *s++ = r; + } +} diff --git a/system/sdl/AudioBackend.h b/system/sdl/AudioBackend.h new file mode 100644 index 0000000..ca1431c --- /dev/null +++ b/system/sdl/AudioBackend.h @@ -0,0 +1,26 @@ +#ifndef GAME_AUDIOOUTPUT_H +#define GAME_AUDIOOUTPUT_H + +#include + +namespace Opl { + void generateSample(int16_t *left, int16_t *right); +} + +class AudioBackend { +public: + AudioBackend(); + ~AudioBackend(); + + void init(); + void terminate(); + + void generate(Uint8 *stream, int len); +private: + SDL_AudioDeviceID _audioDevice; + SDL_AudioSpec _audioSpec; +}; + +extern AudioBackend audioBackend; + +#endif //GAME_AUDIOOUTPUT_H \ No newline at end of file diff --git a/system/sdl/Events.cpp b/system/sdl/Events.cpp new file mode 100644 index 0000000..5311d56 --- /dev/null +++ b/system/sdl/Events.cpp @@ -0,0 +1,25 @@ +#include +#include "Events.h" + +#include "Keyboard.h" + +Events events; + +void Events::poll() { + SDL_Event event; + while (SDL_PollEvent(&event)) { + // TODO handle events + switch (event.type) { + case SDL_QUIT: + // TODO + exit(0); + break; + case SDL_KEYDOWN: + keyboard.keyDown(event.key.keysym.sym); + break; + case SDL_KEYUP: + keyboard.keyUp(event.key.keysym.sym); + break; + } + } +} diff --git a/system/sdl/Events.h b/system/sdl/Events.h new file mode 100644 index 0000000..b9ab17b --- /dev/null +++ b/system/sdl/Events.h @@ -0,0 +1,14 @@ +#ifndef GAME_EVENTS_H +#define GAME_EVENTS_H + +class Events { +public: + Events() = default; + ~Events() = default; + + void poll(); +}; + +extern Events events; + +#endif //GAME_EVENTS_H \ No newline at end of file diff --git a/system/sdl/Keyboard.cpp b/system/sdl/Keyboard.cpp new file mode 100644 index 0000000..f3fc090 --- /dev/null +++ b/system/sdl/Keyboard.cpp @@ -0,0 +1,821 @@ +#include +#include "../Keyboard.h" + +Keyboard keyboard; + +// TODO map unknown key codes +uint8_t mapSdlKeycode(SDL_Keycode keyCode) { + switch (keyCode) { + case SDLK_UNKNOWN: + return KeyUnknown; + case SDLK_RETURN: + return KeyUnknown; + case SDLK_ESCAPE: + return KeyEscape; + case SDLK_BACKSPACE: + return KeyBackspace; + case SDLK_TAB: + return KeyTab; + case SDLK_SPACE: + return KeySpace; + case SDLK_EXCLAIM: + return KeyUnknown; + case SDLK_QUOTEDBL: + return KeyUnknown; + case SDLK_HASH: + return KeyUnknown; + case SDLK_PERCENT: + return KeyUnknown; + case SDLK_DOLLAR: + return KeyUnknown; + case SDLK_AMPERSAND: + return KeyUnknown; + case SDLK_QUOTE: + return KeyUnknown; + case SDLK_LEFTPAREN: + return KeyUnknown; + case SDLK_RIGHTPAREN: + return KeyUnknown; + case SDLK_ASTERISK: + return KeyUnknown; + case SDLK_PLUS: + return KeyUnknown; + case SDLK_COMMA: + return KeyUnknown; + case SDLK_MINUS: + return KeyUnknown; + case SDLK_PERIOD: + return KeyUnknown; + case SDLK_SLASH: + return KeyUnknown; + case SDLK_0: + return Key0; + case SDLK_1: + return Key1; + case SDLK_2: + return Key2; + case SDLK_3: + return Key3; + case SDLK_4: + return Key4; + case SDLK_5: + return Key5; + case SDLK_6: + return Key6; + case SDLK_7: + return Key7; + case SDLK_8: + return Key8; + case SDLK_9: + return Key9; + case SDLK_COLON: + return KeyUnknown; + case SDLK_SEMICOLON: + return KeyUnknown; + case SDLK_LESS: + return KeyUnknown; + case SDLK_EQUALS: + return KeyUnknown; + case SDLK_GREATER: + return KeyUnknown; + case SDLK_QUESTION: + return KeyUnknown; + case SDLK_AT: + return KeyUnknown; + case SDLK_LEFTBRACKET: + return KeyUnknown; + case SDLK_BACKSLASH: + return KeyUnknown; + case SDLK_RIGHTBRACKET: + return KeyUnknown; + case SDLK_CARET: + return KeyUnknown; + case SDLK_UNDERSCORE: + return KeyUnknown; + case SDLK_BACKQUOTE: + return KeyUnknown; + case SDLK_a: + return KeyA; + case SDLK_b: + return KeyB; + case SDLK_c: + return KeyC; + case SDLK_d: + return KeyD; + case SDLK_e: + return KeyE; + case SDLK_f: + return KeyF; + case SDLK_g: + return KeyG; + case SDLK_h: + return KeyG; + case SDLK_i: + return KeyI; + case SDLK_j: + return KeyJ; + case SDLK_k: + return KeyK; + case SDLK_l: + return KeyL; + case SDLK_m: + return KeyM; + case SDLK_n: + return KeyN; + case SDLK_o: + return KeyO; + case SDLK_p: + return KeyP; + case SDLK_q: + return KeyQ; + case SDLK_r: + return KeyR; + case SDLK_s: + return KeyS; + case SDLK_t: + return KeyT; + case SDLK_u: + return KeyU; + case SDLK_v: + return KeyV; + case SDLK_w: + return KeyW; + case SDLK_x: + return KeyX; + case SDLK_y: + return KeyY; + case SDLK_z: + return KeyZ; + case SDLK_CAPSLOCK: + return KeyCapsLock; + case SDLK_F1: + return KeyF1; + case SDLK_F2: + return KeyF2; + case SDLK_F3: + return KeyF3; + case SDLK_F4: + return KeyF4; + case SDLK_F5: + return KeyF5; + case SDLK_F6: + return KeyF6; + case SDLK_F7: + return KeyF7; + case SDLK_F8: + return KeyF8; + case SDLK_F9: + return KeyF9; + case SDLK_F10: + return KeyF10; + case SDLK_F11: + return KeyF11; + case SDLK_F12: + return KeyF12; + case SDLK_PRINTSCREEN: + return KeyPrint; + case SDLK_SCROLLLOCK: + return KeyScrollLock; + case SDLK_PAUSE: + return KeyPause; + case SDLK_INSERT: + return KeyInsert; + case SDLK_HOME: + return KeyHome; + case SDLK_PAGEUP: + return KeyPageUp; + case SDLK_DELETE: + return KeyDelete; + case SDLK_END: + return KeyEnd; + case SDLK_PAGEDOWN: + return KeyPageDown; + case SDLK_RIGHT: + return KeyRight; + case SDLK_LEFT: + return KeyLeft; + case SDLK_DOWN: + return KeyDown; + case SDLK_UP: + return KeyUp; + case SDLK_NUMLOCKCLEAR: + return KeyUnknown; + case SDLK_KP_DIVIDE: + return KeyUnknown; + case SDLK_KP_MULTIPLY: + return KeyUnknown; + case SDLK_KP_MINUS: + return KeyUnknown; + case SDLK_KP_PLUS: + return KeyUnknown; + case SDLK_KP_ENTER: + return KeyUnknown; + case SDLK_KP_1: + return KeyUnknown; + case SDLK_KP_2: + return KeyUnknown; + case SDLK_KP_3: + return KeyUnknown; + case SDLK_KP_4: + return KeyUnknown; + case SDLK_KP_5: + return KeyUnknown; + case SDLK_KP_6: + return KeyUnknown; + case SDLK_KP_7: + return KeyUnknown; + case SDLK_KP_8: + return KeyUnknown; + case SDLK_KP_9: + return KeyUnknown; + case SDLK_KP_0: + return KeyUnknown; + case SDLK_KP_PERIOD: + return KeyUnknown; + case SDLK_APPLICATION: + return KeyUnknown; + case SDLK_POWER: + return KeyUnknown; + case SDLK_KP_EQUALS: + return KeyUnknown; + case SDLK_F13: + return KeyUnknown; + case SDLK_F14: + return KeyUnknown; + case SDLK_F15: + return KeyUnknown; + case SDLK_F16: + return KeyUnknown; + case SDLK_F17: + return KeyUnknown; + case SDLK_F18: + return KeyUnknown; + case SDLK_F19: + return KeyUnknown; + case SDLK_F20: + return KeyUnknown; + case SDLK_F21: + return KeyUnknown; + case SDLK_F22: + return KeyUnknown; + case SDLK_F23: + return KeyUnknown; + case SDLK_F24: + return KeyUnknown; + case SDLK_EXECUTE: + return KeyUnknown; + case SDLK_HELP: + return KeyUnknown; + case SDLK_MENU: + return KeyUnknown; + case SDLK_SELECT: + return KeyUnknown; + case SDLK_STOP: + return KeyUnknown; + case SDLK_AGAIN: + return KeyUnknown; + case SDLK_UNDO: + return KeyUnknown; + case SDLK_CUT: + return KeyUnknown; + case SDLK_COPY: + return KeyUnknown; + case SDLK_PASTE: + return KeyUnknown; + case SDLK_FIND: + return KeyUnknown; + case SDLK_MUTE: + return KeyUnknown; + case SDLK_VOLUMEUP: + return KeyUnknown; + case SDLK_VOLUMEDOWN: + return KeyUnknown; + case SDLK_KP_COMMA: + return KeyUnknown; + case SDLK_KP_EQUALSAS400: + return KeyUnknown; + case SDLK_ALTERASE: + return KeyUnknown; + case SDLK_SYSREQ: + return KeyUnknown; + case SDLK_CANCEL: + return KeyUnknown; + case SDLK_CLEAR: + return KeyUnknown; + case SDLK_PRIOR: + return KeyUnknown; + case SDLK_RETURN2: + return KeyUnknown; + case SDLK_SEPARATOR: + return KeyUnknown; + case SDLK_OUT: + return KeyUnknown; + case SDLK_OPER: + return KeyUnknown; + case SDLK_CLEARAGAIN: + return KeyUnknown; + case SDLK_CRSEL: + return KeyUnknown; + case SDLK_EXSEL: + return KeyUnknown; + case SDLK_KP_00: + return KeyUnknown; + case SDLK_KP_000: + return KeyUnknown; + case SDLK_THOUSANDSSEPARATOR: + return KeyUnknown; + case SDLK_DECIMALSEPARATOR: + return KeyUnknown; + case SDLK_CURRENCYUNIT: + return KeyUnknown; + case SDLK_CURRENCYSUBUNIT: + return KeyUnknown; + case SDLK_KP_LEFTPAREN: + return KeyUnknown; + case SDLK_KP_RIGHTPAREN: + return KeyUnknown; + case SDLK_KP_LEFTBRACE: + return KeyUnknown; + case SDLK_KP_RIGHTBRACE: + return KeyUnknown; + case SDLK_KP_TAB: + return KeyUnknown; + case SDLK_KP_BACKSPACE: + return KeyUnknown; + case SDLK_KP_A: + return KeyUnknown; + case SDLK_KP_B: + return KeyUnknown; + case SDLK_KP_C: + return KeyUnknown; + case SDLK_KP_D: + return KeyUnknown; + case SDLK_KP_E: + return KeyUnknown; + case SDLK_KP_F: + return KeyUnknown; + case SDLK_KP_XOR: + return KeyUnknown; + case SDLK_KP_POWER: + return KeyUnknown; + case SDLK_KP_PERCENT: + return KeyUnknown; + case SDLK_KP_LESS: + return KeyUnknown; + case SDLK_KP_GREATER: + return KeyUnknown; + case SDLK_KP_AMPERSAND: + return KeyUnknown; + case SDLK_KP_DBLAMPERSAND: + return KeyUnknown; + case SDLK_KP_VERTICALBAR: + return KeyUnknown; + case SDLK_KP_DBLVERTICALBAR: + return KeyUnknown; + case SDLK_KP_COLON: + return KeyUnknown; + case SDLK_KP_HASH: + return KeyUnknown; + case SDLK_KP_SPACE: + return KeyUnknown; + case SDLK_KP_AT: + return KeyUnknown; + case SDLK_KP_EXCLAM: + return KeyUnknown; + case SDLK_KP_MEMSTORE: + return KeyUnknown; + case SDLK_KP_MEMRECALL: + return KeyUnknown; + case SDLK_KP_MEMCLEAR: + return KeyUnknown; + case SDLK_KP_MEMADD: + return KeyUnknown; + case SDLK_KP_MEMSUBTRACT: + return KeyUnknown; + case SDLK_KP_MEMMULTIPLY: + return KeyUnknown; + case SDLK_KP_MEMDIVIDE: + return KeyUnknown; + case SDLK_KP_PLUSMINUS: + return KeyUnknown; + case SDLK_KP_CLEAR: + return KeyUnknown; + case SDLK_KP_CLEARENTRY: + return KeyUnknown; + case SDLK_KP_BINARY: + return KeyUnknown; + case SDLK_KP_OCTAL: + return KeyUnknown; + case SDLK_KP_DECIMAL: + return KeyUnknown; + case SDLK_KP_HEXADECIMAL: + return KeyUnknown; + case SDLK_LCTRL: + return KeyLeftCtrl; + case SDLK_LSHIFT: + return KeyLeftShift; + case SDLK_LALT: + return KeyLeftAlt; + case SDLK_LGUI: + return KeyUnknown; + case SDLK_RCTRL: + return KeyRightControl; + case SDLK_RSHIFT: + return KeyRightShift; + case SDLK_RALT: + return KeyRightAlt; + case SDLK_RGUI: + return KeyUnknown; + case SDLK_MODE: + return KeyUnknown; + case SDLK_AUDIONEXT: + return KeyUnknown; + case SDLK_AUDIOPREV: + return KeyUnknown; + case SDLK_AUDIOSTOP: + return KeyUnknown; + case SDLK_AUDIOPLAY: + return KeyUnknown; + case SDLK_AUDIOMUTE: + return KeyUnknown; + case SDLK_MEDIASELECT: + return KeyUnknown; + case SDLK_WWW: + return KeyUnknown; + case SDLK_MAIL: + return KeyUnknown; + case SDLK_CALCULATOR: + return KeyUnknown; + case SDLK_COMPUTER: + return KeyUnknown; + case SDLK_AC_SEARCH: + return KeyUnknown; + case SDLK_AC_HOME: + return KeyUnknown; + case SDLK_AC_BACK: + return KeyUnknown; + case SDLK_AC_FORWARD: + return KeyUnknown; + case SDLK_AC_STOP: + return KeyUnknown; + case SDLK_AC_REFRESH: + return KeyUnknown; + case SDLK_AC_BOOKMARKS: + return KeyUnknown; + case SDLK_BRIGHTNESSDOWN: + return KeyUnknown; + case SDLK_BRIGHTNESSUP: + return KeyUnknown; + case SDLK_DISPLAYSWITCH: + return KeyUnknown; + case SDLK_KBDILLUMTOGGLE: + return KeyUnknown; + case SDLK_KBDILLUMDOWN: + return KeyUnknown; + case SDLK_KBDILLUMUP: + return KeyUnknown; + case SDLK_EJECT: + return KeyUnknown; + case SDLK_SLEEP: + return KeyUnknown; + case SDLK_APP1: + return KeyUnknown; + case SDLK_APP2: + return KeyUnknown; + case SDLK_AUDIOREWIND: + return KeyUnknown; + case SDLK_AUDIOFASTFORWARD: + return KeyUnknown; + default: ; + } + + return KeyUnknown; +} + +static const char *keyNames[] = { + "", + "KeyEscape", // = 0x01, + "Key1", // = 0x02, + "Key2", // = 0x03, + "Key3", // = 0x04, + "Key4", // = 0x05, + "Key5", // = 0x06, + "Key6", // = 0x07, + "Key7", // = 0x08, + "Key8", // = 0x09, + "Key9", // = 0x0a, + "Key0", // = 0x0b, + "KeyMinus", // = 0x0c, + "KeyEqual", // = 0x0d, + "KeyBackspace", // = 0x0e, + "KeyTab", // = 0x0f, + + "KeyQ", // = 0x10, + "KeyW", // = 0x11, + "KeyE", // = 0x12, + "KeyR", // = 0x13, + "KeyT", // = 0x14, + "KeyY", // = 0x15, + "KeyU", // = 0x16, + "KeyI", // = 0x17, + "KeyO", // = 0x18, + "KeyP", // = 0x19, + "KeyBracketLeft", // = 0x1a, + "KeyBracketRight", // = 0x1b, + "KeyEnter", // = 0x1c, + "KeyLeftCtrl", // = 0x1d, + "KeyA", // = 0x1e, + "KeyS", // = 0x1f, + + "KeyD", // = 0x20, + "KeyF", // = 0x21, + "KeyG", // = 0x22, + "KeyH", // = 0x23, + "KeyJ", // = 0x24, + "KeyK", // = 0x25, + "KeyL", // = 0x26, + "KeySemicolon", // = 0x27, + "KeyApostrophe", // = 0x28, + "KeyBacktick", // = 0x29, + "KeyLeftShift", // = 0x2a, + "KeyBackslash", // = 0x2b, + "KeyZ", // = 0x2c, + "KeyX", // = 0x2d, + "KeyC", // = 0x2e, + "KeyV", // = 0x2f, + + "KeyB", // = 0x30, + "KeyN", // = 0x31, + "KeyM", // = 0x32, + "KeyComma", // = 0x33, + "KeyPeriod", // = 0x34, + "KeySlash", // = 0x35, + "KeyRightShift", // = 0x36, + "KeyKeypadMultiply", // = 0x37, + "KeyLeftAlt", // = 0x38, + "KeySpace", // = 0x39, + "KeyCapsLock", // = 0x3a, + "KeyF1", // = 0x3b, + "KeyF2", // = 0x3c, + "KeyF3", // = 0x3d, + "KeyF4", // = 0x3e, + "KeyF5", // = 0x3f, + + "KeyF6", // = 0x40, + "KeyF7", // = 0x41, + "KeyF8", // = 0x42, + "KeyF9", // = 0x43, + "KeyF10", // = 0x44, + "KeyNumLock", // = 0x45, + "KeyScrollLock", // = 0x46, + "KeyKeypad7", // = 0x47, + "KeyKeypad8", // = 0x48, + "KeyKeypad9", // = 0x49, + "KeyKeypadMinus", // = 0x4a, + "KeyKeypad4", // = 0x4b, + "KeyKeypad5", // = 0x4c, + "KeyKeypad6", // = 0x4d, + "KeyKeypadPlus", // = 0x4e, + "KeyKeypad1", // = 0x4f, + + "KeyKeypad2", // = 0x50, + "KeyKeypad3", // = 0x51, + "KeyKeypad0", // = 0x52, + "KeyKeypadPeriod", // = 0x53, + "", + "", + "", + "KeyF11", // = 0x57, + "KeyF12", // = 0x58, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + + "KeyMediaPrev", // = 0x90, + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaNext", // = 0x99, + "", + "", + "KeyKeypadEnter", // = 0x9c, + "KeyRightControl", // = 0x9d, + "", + "", + + "KeyMediaMute", // = 0xa0, + "KeyMediaCalculator", // = 0xa1, + "KeyMediaPlay", // = 0xa2, + "", + "KeyMediaStop", // = 0xa4, + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyMediaVolumeDown", // = 0xae, + "", + + "KeyMediaVolumeUp", // = 0xb0, + "", + "KeyMediaWww", // = 0xb2, + "", + "", + "KeyKeypadDivide", // = 0xb5, + "", + "", + "KeyRightAlt", // = 0xb8, + "", + "", + "", + "", + "", + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "KeyHome", // = 0xc7, + "KeyUp", // = 0xc8, + "KeyPageUp", // = 0xc9, + "", + "KeyLeft", // = 0xcb, + "", + "KeyRight", // = 0xcd, + "", + "KeyEnd", // = 0xcf, + + "KeyDown", // = 0xd0, + "KeyPageDown", // = 0xd1, + "KeyInsert", // = 0xd2, + "KeyDelete", // = 0xd3, + "", + "", + "", + "", + "", + "", + "", + "KeyLeftGui", // = 0xdb, + "KeyRightGui", // = 0xdc, + "KeyApps", // = 0xdd, + "KeyAcpiPower", // = 0xde, + "KeyAcpiSleep", // = 0xdf, + + "", + "", + "", + "KeyAcpiWake", // = 0xe3, + "", + "KeyMediaWwwSearch", // = 0xe5, + "KeyMediaWwwFavorites", // = 0xe6, + "KeyMediaWwwRefresh", // = 0xe7, + "KeyMediaWwwStop", // = 0xe8, + "KeyMediaWwwForward", // = 0xe9, + "KeyMediaWwwBack", // = 0xea, + "KeyMediaMyComputer", // = 0xeb, + "KeyMediaEmail", // = 0xec, + "KeyMediaSelect", // = 0xed, + "", + "", + + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "KeyPrint", // = 0xfe, + "KeyPause", // = 0xff, +}; + +void keyboardIsr() { + keyboard.Isr(); +} + +Keyboard::Keyboard() { + // TODO +} + +Keyboard::~Keyboard() { + // TODO +} + +Keyboard::KeyHandleFunction Keyboard::setKeyUpHandler(KeyHandleFunction handler) { + auto oldHandler = _keyUpHandler; + _keyUpHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyDownHandler(KeyHandleFunction handler) { + auto oldHandler = _keyDownHandler; + _keyDownHandler = handler; + return oldHandler; +} + +Keyboard::KeyHandleFunction Keyboard::setKeyRepeatHandler(KeyHandleFunction handler) { + auto oldHandler = _keyRepeatHandler; + _keyRepeatHandler = handler; + return oldHandler; +} + +const char *Keyboard::getKeyName(unsigned char keyCode) { + return keyNames[keyCode]; +} + +void Keyboard::Isr() { + // TODO +} + +void Keyboard::keyDown(SDL_Keycode keyCode) { + auto c = mapSdlKeycode(keyCode); + if (keyState[c]) { + if (_keyRepeatHandler) _keyRepeatHandler(c); + } else { + keyState[c] = true; + if (_keyDownHandler) _keyDownHandler(c); + } +} + +void Keyboard::keyUp(SDL_Keycode keyCode) { + auto c = mapSdlKeycode(keyCode); + keyState[c] = false; + if (_keyUpHandler) _keyUpHandler(c); +} diff --git a/system/sdl/Keyboard.h b/system/sdl/Keyboard.h new file mode 100644 index 0000000..3085602 --- /dev/null +++ b/system/sdl/Keyboard.h @@ -0,0 +1,178 @@ +#ifndef GAME_KEYBOARD_H +#define GAME_KEYBOARD_H + +#include + +#define KEY_MAX 256 + +enum { + KeyUnknown = 0x00, + KeyEscape = 0x01, + Key1 = 0x02, + Key2 = 0x03, + Key3 = 0x04, + Key4 = 0x05, + Key5 = 0x06, + Key6 = 0x07, + Key7 = 0x08, + Key8 = 0x09, + Key9 = 0x0a, + Key0 = 0x0b, + KeyMinus = 0x0c, + KeyEqual = 0x0d, + KeyBackspace = 0x0e, + KeyTab = 0x0f, + KeyQ = 0x10, + KeyW = 0x11, + KeyE = 0x12, + KeyR = 0x13, + KeyT = 0x14, + KeyY = 0x15, + KeyU = 0x16, + KeyI = 0x17, + KeyO = 0x18, + KeyP = 0x19, + KeyBracketLeft = 0x1a, + KeyBracketRight = 0x1b, + KeyEnter = 0x1c, + KeyLeftCtrl = 0x1d, + KeyA = 0x1e, + KeyS = 0x1f, + KeyD = 0x20, + KeyF = 0x21, + KeyG = 0x22, + KeyH = 0x23, + KeyJ = 0x24, + KeyK = 0x25, + KeyL = 0x26, + KeySemicolon = 0x27, + KeyApostrophe = 0x28, + KeyBacktick = 0x29, + KeyLeftShift = 0x2a, + KeyBackslash = 0x2b, + KeyZ = 0x2c, + KeyX = 0x2d, + KeyC = 0x2e, + KeyV = 0x2f, + KeyB = 0x30, + KeyN = 0x31, + KeyM = 0x32, + KeyComma = 0x33, + KeyPeriod = 0x34, + KeySlash = 0x35, + KeyRightShift = 0x36, + KeyKeypadMultiply = 0x37, + KeyLeftAlt = 0x38, + KeySpace = 0x39, + KeyCapsLock = 0x3a, + KeyF1 = 0x3b, + KeyF2 = 0x3c, + KeyF3 = 0x3d, + KeyF4 = 0x3e, + KeyF5 = 0x3f, + KeyF6 = 0x40, + KeyF7 = 0x41, + KeyF8 = 0x42, + KeyF9 = 0x43, + KeyF10 = 0x44, + KeyNumLock = 0x45, + KeyScrollLock = 0x46, + KeyKeypad7 = 0x47, + KeyKeypad8 = 0x48, + KeyKeypad9 = 0x49, + KeyKeypadMinus = 0x4a, + KeyKeypad4 = 0x4b, + KeyKeypad5 = 0x4c, + KeyKeypad6 = 0x4d, + KeyKeypadPlus = 0x4e, + KeyKeypad1 = 0x4f, + KeyKeypad2 = 0x50, + KeyKeypad3 = 0x51, + KeyKeypad0 = 0x52, + KeyKeypadPeriod = 0x53, + KeyF11 = 0x57, + KeyF12 = 0x58, + KeyMediaPrev = 0x90, + KeyMediaNext = 0x99, + KeyKeypadEnter = 0x9c, + KeyRightControl = 0x9d, + KeyMediaMute = 0xa0, + KeyMediaCalculator = 0xa1, + KeyMediaPlay = 0xa2, + KeyMediaStop = 0xa4, + KeyMediaVolumeDown = 0xae, + KeyMediaVolumeUp = 0xb0, + KeyMediaWww = 0xb2, + KeyKeypadDivide = 0xb5, + KeyRightAlt = 0xb8, + KeyHome = 0xc7, + KeyUp = 0xc8, + KeyPageUp = 0xc9, + KeyLeft = 0xcb, + KeyRight = 0xcd, + KeyEnd = 0xcf, + KeyDown = 0xd0, + KeyPageDown = 0xd1, + KeyInsert = 0xd2, + KeyDelete = 0xd3, + KeyLeftGui = 0xdb, + KeyRightGui = 0xdc, + KeyApps = 0xdd, + KeyAcpiPower = 0xde, + KeyAcpiSleep = 0xdf, + KeyAcpiWake = 0xe3, + KeyMediaWwwSearch = 0xe5, + KeyMediaWwwFavorites = 0xe6, + KeyMediaWwwRefresh = 0xe7, + KeyMediaWwwStop = 0xe8, + KeyMediaWwwForward = 0xe9, + KeyMediaWwwBack = 0xea, + KeyMediaMyComputer = 0xeb, + KeyMediaEmail = 0xec, + KeyMediaSelect = 0xed, + KeyPrint = 0xfe, + KeyPause = 0xff, +}; + +class Keyboard { +public: + using KeyHandleFunction = void (*)(unsigned char); + + Keyboard(); + ~Keyboard(); + + KeyHandleFunction setKeyUpHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyDownHandler(KeyHandleFunction handler); + KeyHandleFunction setKeyRepeatHandler(KeyHandleFunction handler); + + bool keyState[KEY_MAX]{}; + + const char *getKeyName(unsigned char keyCode); +private: + friend class Events; + + friend void keyboardIsr(); + void Isr(); + enum State { + Start, + Extended, + PauseBegin, + PausePressed1, + PauseReleased1, + PrintPressed1, + PrintPressed2, + PrintReleased1, + PrintReleased2, + }; + State _state; + KeyHandleFunction _keyDownHandler{nullptr}, _keyUpHandler{nullptr}, _keyRepeatHandler{nullptr}; + + void keyDown(SDL_Keycode c); + + void keyUp(SDL_Keycode c); + +}; + +extern Keyboard keyboard; + +#endif //GAME_KEYBOARD_H diff --git a/system/sdl/Opl.cpp b/system/sdl/Opl.cpp new file mode 100644 index 0000000..c0b814e --- /dev/null +++ b/system/sdl/Opl.cpp @@ -0,0 +1,1348 @@ +#include + +#include "../Opl.h" + +/* + + The Opal OPL3 emulator. + + Note: this is not a complete emulator, just enough for Reality Adlib Tracker tunes. + + Missing features compared to a real OPL3: + + - Timers/interrupts + - OPL3 enable bit (it defaults to always on) + - CSW mode + - Test register + - Percussion mode + +*/ + +#include + +//================================================================================================== +// Opal class. +//================================================================================================== +class Opal { + + class Channel; + + // Various constants + enum { + OPL3SampleRate = 49716, + NumChannels = 18, + NumOperators = 36, + + EnvOff = -1, + EnvAtt, + EnvDec, + EnvSus, + EnvRel, + }; + + // A single FM operator + class Operator { + + public: + Operator(); + void SetMaster(Opal *opal) { Master = opal; } + void SetChannel(Channel *chan) { Chan = chan; } + + int16_t Output(uint16_t keyscalenum, uint32_t phase_step, int16_t vibrato, int16_t mod = 0, int16_t fbshift = 0); + + void SetKeyOn(bool on); + void SetTremoloEnable(bool on); + void SetVibratoEnable(bool on); + void SetSustainMode(bool on); + void SetEnvelopeScaling(bool on); + void SetFrequencyMultiplier(uint16_t scale); + void SetKeyScale(uint16_t scale); + void SetOutputLevel(uint16_t level); + void SetAttackRate(uint16_t rate); + void SetDecayRate(uint16_t rate); + void SetSustainLevel(uint16_t level); + void SetReleaseRate(uint16_t rate); + void SetWaveform(uint16_t wave); + + void ComputeRates(); + void ComputeKeyScaleLevel(); + + protected: + Opal * Master; // Master object + Channel * Chan; // Owning channel + uint32_t Phase; // The current offset in the selected waveform + uint16_t Waveform; // The waveform id this operator is using + uint16_t FreqMultTimes2; // Frequency multiplier * 2 + int EnvelopeStage; // Which stage the envelope is at (see Env* enums above) + int16_t EnvelopeLevel; // 0 - $1FF, 0 being the loudest + uint16_t OutputLevel; // 0 - $FF + uint16_t AttackRate; + uint16_t DecayRate; + uint16_t SustainLevel; + uint16_t ReleaseRate; + uint16_t AttackShift; + uint16_t AttackMask; + uint16_t AttackAdd; + const uint16_t *AttackTab; + uint16_t DecayShift; + uint16_t DecayMask; + uint16_t DecayAdd; + const uint16_t *DecayTab; + uint16_t ReleaseShift; + uint16_t ReleaseMask; + uint16_t ReleaseAdd; + const uint16_t *ReleaseTab; + uint16_t KeyScaleShift; + uint16_t KeyScaleLevel; + int16_t Out[2]; + bool KeyOn; + bool KeyScaleRate; // Affects envelope rate scaling + bool SustainMode; // Whether to sustain during the sustain phase, or release instead + bool TremoloEnable; + bool VibratoEnable; + }; + + // A single channel, which can contain two or more operators + class Channel { + + public: + Channel(); + void SetMaster(Opal *opal) { Master = opal; } + void SetOperators(Operator *a, Operator *b, Operator *c, Operator *d) { + Op[0] = a; + Op[1] = b; + Op[2] = c; + Op[3] = d; + if (a) + a->SetChannel(this); + if (b) + b->SetChannel(this); + if (c) + c->SetChannel(this); + if (d) + d->SetChannel(this); + } + + void Output(int16_t &left, int16_t &right); + void SetEnable(bool on) { Enable = on; } + void SetChannelPair(Channel *pair) { ChannelPair = pair; } + + void SetFrequencyLow(uint16_t freq); + void SetFrequencyHigh(uint16_t freq); + void SetKeyOn(bool on); + void SetOctave(uint16_t oct); + void SetLeftEnable(bool on); + void SetRightEnable(bool on); + void SetFeedback(uint16_t val); + void SetModulationType(uint16_t type); + + uint16_t GetFreq() const { return Freq; } + uint16_t GetOctave() const { return Octave; } + uint16_t GetKeyScaleNumber() const { return KeyScaleNumber; } + uint16_t GetModulationType() const { return ModulationType; } + + void ComputeKeyScaleNumber(); + + protected: + void ComputePhaseStep(); + + Operator * Op[4]; + + Opal * Master; // Master object + uint16_t Freq; // Frequency; actually it's a phase stepping value + uint16_t Octave; // Also known as "block" in Yamaha parlance + uint32_t PhaseStep; + uint16_t KeyScaleNumber; + uint16_t FeedbackShift; + uint16_t ModulationType; + Channel * ChannelPair; + bool Enable; + bool LeftEnable, RightEnable; + }; + + public: + Opal(int sample_rate); + ~Opal(); + + void SetSampleRate(int sample_rate); + void Port(uint16_t reg_num, uint8_t val); + void Sample(int16_t *left, int16_t *right); + + protected: + void Init(int sample_rate); + void Output(int16_t &left, int16_t &right); + + int32_t SampleRate; + int32_t SampleAccum; + int16_t LastOutput[2], CurrOutput[2]; + Channel Chan[NumChannels]; + Operator Op[NumOperators]; +// uint16_t ExpTable[256]; +// uint16_t LogSinTable[256]; + uint16_t Clock; + uint16_t TremoloClock; + uint16_t TremoloLevel; + uint16_t VibratoTick; + uint16_t VibratoClock; + bool NoteSel; + bool TremoloDepth; + bool VibratoDepth; + + static const uint16_t RateTables[4][8]; + static const uint16_t ExpTable[256]; + static const uint16_t LogSinTable[256]; +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::RateTables[4][8] = { + { 1, 0, 1, 0, 1, 0, 1, 0 }, + { 1, 0, 1, 0, 0, 0, 1, 0 }, + { 1, 0, 0, 0, 1, 0, 0, 0 }, + { 1, 0, 0, 0, 0, 0, 0, 0 }, +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::ExpTable[0x100] = { + 1018, 1013, 1007, 1002, 996, 991, 986, 980, 975, 969, 964, 959, 953, 948, 942, 937, + 932, 927, 921, 916, 911, 906, 900, 895, 890, 885, 880, 874, 869, 864, 859, 854, + 849, 844, 839, 834, 829, 824, 819, 814, 809, 804, 799, 794, 789, 784, 779, 774, + 770, 765, 760, 755, 750, 745, 741, 736, 731, 726, 722, 717, 712, 708, 703, 698, + 693, 689, 684, 680, 675, 670, 666, 661, 657, 652, 648, 643, 639, 634, 630, 625, + 621, 616, 612, 607, 603, 599, 594, 590, 585, 581, 577, 572, 568, 564, 560, 555, + 551, 547, 542, 538, 534, 530, 526, 521, 517, 513, 509, 505, 501, 496, 492, 488, + 484, 480, 476, 472, 468, 464, 460, 456, 452, 448, 444, 440, 436, 432, 428, 424, + 420, 416, 412, 409, 405, 401, 397, 393, 389, 385, 382, 378, 374, 370, 367, 363, + 359, 355, 352, 348, 344, 340, 337, 333, 329, 326, 322, 318, 315, 311, 308, 304, + 300, 297, 293, 290, 286, 283, 279, 276, 272, 268, 265, 262, 258, 255, 251, 248, + 244, 241, 237, 234, 231, 227, 224, 220, 217, 214, 210, 207, 204, 200, 197, 194, + 190, 187, 184, 181, 177, 174, 171, 168, 164, 161, 158, 155, 152, 148, 145, 142, + 139, 136, 133, 130, 126, 123, 120, 117, 114, 111, 108, 105, 102, 99, 96, 93, + 90, 87, 84, 81, 78, 75, 72, 69, 66, 63, 60, 57, 54, 51, 48, 45, + 42, 40, 37, 34, 31, 28, 25, 22, 20, 17, 14, 11, 8, 6, 3, 0, +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::LogSinTable[0x100] = { + 2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137, 1091, 1050, 1013, 979, 949, 920, 894, 869, + 846, 825, 804, 785, 767, 749, 732, 717, 701, 687, 672, 659, 646, 633, 621, 609, + 598, 587, 576, 566, 556, 546, 536, 527, 518, 509, 501, 492, 484, 476, 468, 461, + 453, 446, 439, 432, 425, 418, 411, 405, 399, 392, 386, 380, 375, 369, 363, 358, + 352, 347, 341, 336, 331, 326, 321, 316, 311, 307, 302, 297, 293, 289, 284, 280, + 276, 271, 267, 263, 259, 255, 251, 248, 244, 240, 236, 233, 229, 226, 222, 219, + 215, 212, 209, 205, 202, 199, 196, 193, 190, 187, 184, 181, 178, 175, 172, 169, + 167, 164, 161, 159, 156, 153, 151, 148, 146, 143, 141, 138, 136, 134, 131, 129, + 127, 125, 122, 120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96, + 94, 92, 91, 89, 87, 85, 83, 82, 80, 78, 77, 75, 74, 72, 70, 69, + 67, 66, 64, 63, 62, 60, 59, 57, 56, 55, 53, 52, 51, 49, 48, 47, + 46, 45, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, + 29, 28, 27, 26, 25, 24, 23, 23, 22, 21, 20, 20, 19, 18, 17, 17, + 16, 15, 15, 14, 13, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, + 7, 7, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, + 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, +}; + + + +//================================================================================================== +// This is the temporary code for generating the above tables. Maths and data from this nice +// reverse-engineering effort: +// +// https://docs.google.com/document/d/18IGx18NQY_Q1PJVZ-bHywao9bhsDoAqoIn1rIm42nwo/edit +//================================================================================================== +#if 0 +#include + +void GenerateTables() { + + // Build the exponentiation table (reversed from the official OPL3 ROM) + FILE *fd = fopen("exptab.txt", "wb"); + if (fd) { + for (int i = 0; i < 0x100; i++) { + int v = (pow(2, (0xFF - i) / 256.0) - 1) * 1024 + 0.5; + if (i & 15) + fprintf(fd, " %4d,", v); + else + fprintf(fd, "\n\t%4d,", v); + } + fclose(fd); + } + + // Build the log-sin table + fd = fopen("sintab.txt", "wb"); + if (fd) { + for (int i = 0; i < 0x100; i++) { + int v = -log(sin((i + 0.5) * 3.1415926535897933 / 256 / 2)) / log(2) * 256 + 0.5; + if (i & 15) + fprintf(fd, " %4d,", v); + else + fprintf(fd, "\n\t%4d,", v); + } + fclose(fd); + } +} +#endif + + + +//================================================================================================== +// Constructor/destructor. +//================================================================================================== +Opal::Opal(int sample_rate) { + + Init(sample_rate); +} +//-------------------------------------------------------------------------------------------------- +Opal::~Opal() { +} + + + +//================================================================================================== +// Initialise the emulation. +//================================================================================================== +void Opal::Init(int sample_rate) { + + Clock = 0; + TremoloClock = 0; + VibratoTick = 0; + VibratoClock = 0; + NoteSel = false; + TremoloDepth = false; + VibratoDepth = false; + +// // Build the exponentiation table (reversed from the official OPL3 ROM) +// for (int i = 0; i < 0x100; i++) +// ExpTable[i] = (pow(2, (0xFF - i) / 256.0) - 1) * 1024 + 0.5; +// +// // Build the log-sin table +// for (int i = 0; i < 0x100; i++) +// LogSinTable[i] = -log(sin((i + 0.5) * 3.1415926535897933 / 256 / 2)) / log(2) * 256 + 0.5; + + // Let sub-objects know where to find us + for (int i = 0; i < NumOperators; i++) + Op[i].SetMaster(this); + + for (int i = 0; i < NumChannels; i++) + Chan[i].SetMaster(this); + + // Add the operators to the channels. Note, some channels can't use all the operators + // FIXME: put this into a separate routine + const int chan_ops[] = { + 0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20, 24, 25, 26, 30, 31, 32, + }; + + for (int i = 0; i < NumChannels; i++) { + Channel *chan = &Chan[i]; + int op = chan_ops[i]; + if (i < 3 || (i >= 9 && i < 12)) + chan->SetOperators(&Op[op], &Op[op + 3], &Op[op + 6], &Op[op + 9]); + else + chan->SetOperators(&Op[op], &Op[op + 3], 0, 0); + } + + // Initialise the operator rate data. We can't do this in the Operator constructor as it + // relies on referencing the master and channel objects + for (int i = 0; i < NumOperators; i++) + Op[i].ComputeRates(); + + SetSampleRate(sample_rate); +} + + + +//================================================================================================== +// Change the sample rate. +//================================================================================================== +void Opal::SetSampleRate(int sample_rate) { + + // Sanity + if (sample_rate == 0) + sample_rate = OPL3SampleRate; + + SampleRate = sample_rate; + SampleAccum = 0; + LastOutput[0] = LastOutput[1] = 0; + CurrOutput[0] = CurrOutput[1] = 0; +} + + + +//================================================================================================== +// Write a value to an OPL3 register. +//================================================================================================== +void Opal::Port(uint16_t reg_num, uint8_t val) { + + const int op_lookup[] = { + // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + 0, 1, 2, 3, 4, 5, -1, -1, 6, 7, 8, 9, 10, 11, -1, -1, + // 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F + 12, 13, 14, 15, 16, 17, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + uint16_t type = reg_num & 0xE0; + + // Is it BD, the one-off register stuck in the middle of the register array? + if (reg_num == 0xBD) { + TremoloDepth = (val & 0x80); + VibratoDepth = (val & 0x40); + return; + } + + // Global registers + if (type == 0x00) { + + // 4-OP enables + if (reg_num == 0x104) { + + // Enable/disable channels based on which 4-op enables + uint8_t mask = 1; + for (int i = 0; i < 6; i++, mask <<= 1) { + + // The 4-op channels are 0, 1, 2, 9, 10, 11 + uint16_t chan = i < 3 ? i : i + 6; + Channel *primary = &Chan[chan]; + Channel *secondary = &Chan[chan + 3]; + + if (val & mask) { + + // Let primary channel know it's controlling the secondary channel + primary->SetChannelPair(secondary); + + // Turn off the second channel in the pair + secondary->SetEnable(false); + + } else { + + // Let primary channel know it's no longer controlling the secondary channel + primary->SetChannelPair(0); + + // Turn on the second channel in the pair + secondary->SetEnable(true); + } + } + + // CSW / Note-sel + } else if (reg_num == 0x08) { + + NoteSel = (val & 0x40); + + // Get the channels to recompute the Key Scale No. as this varies based on NoteSel + for (int i = 0; i < NumChannels; i++) + Chan[i].ComputeKeyScaleNumber(); + } + + // Channel registers + } else if (type >= 0xA0 && type <= 0xC0) { + + // Convert to channel number + int chan_num = reg_num & 15; + + // Valid channel? + if (chan_num >= 9) + return; + + // Is it the other bank of channels? + if (reg_num & 0x100) + chan_num += 9; + + Channel &chan = Chan[chan_num]; + + // Do specific registers + switch (reg_num & 0xF0) { + + // Frequency low + case 0xA0: { + chan.SetFrequencyLow(val); + break; + } + + // Key-on / Octave / Frequency High + case 0xB0: { + chan.SetKeyOn(val & 0x20); + chan.SetOctave(val >> 2 & 7); + chan.SetFrequencyHigh(val & 3); + break; + } + + // Right Stereo Channel Enable / Left Stereo Channel Enable / Feedback Factor / Modulation Type + case 0xC0: { + chan.SetRightEnable(val & 0x20); + chan.SetLeftEnable(val & 0x10); + chan.SetFeedback(val >> 1 & 7); + chan.SetModulationType(val & 1); + break; + } + } + + // Operator registers + } else if ((type >= 0x20 && type <= 0x80) || type == 0xE0) { + + // Convert to operator number + int op_num = op_lookup[reg_num & 0x1F]; + + // Valid register? + if (op_num < 0) + return; + + // Is it the other bank of operators? + if (reg_num & 0x100) + op_num += 18; + + Operator &op = Op[op_num]; + + // Do specific registers + switch (type) { + + // Tremolo Enable / Vibrato Enable / Sustain Mode / Envelope Scaling / Frequency Multiplier + case 0x20: { + op.SetTremoloEnable(val & 0x80); + op.SetVibratoEnable(val & 0x40); + op.SetSustainMode(val & 0x20); + op.SetEnvelopeScaling(val & 0x10); + op.SetFrequencyMultiplier(val & 15); + break; + } + + // Key Scale / Output Level + case 0x40: { + op.SetKeyScale(val >> 6); + op.SetOutputLevel(val & 0x3F); + break; + } + + // Attack Rate / Decay Rate + case 0x60: { + op.SetAttackRate(val >> 4); + op.SetDecayRate(val & 15); + break; + } + + // Sustain Level / Release Rate + case 0x80: { + op.SetSustainLevel(val >> 4); + op.SetReleaseRate(val & 15); + break; + } + + // Waveform + case 0xE0: { + op.SetWaveform(val & 7); + break; + } + } + } +} + + + +//================================================================================================== +// Generate sample. Every time you call this you will get two signed 16-bit samples (one for each +// stereo channel) which will sound correct when played back at the sample rate given when the +// class was constructed. +//================================================================================================== +void Opal::Sample(int16_t *left, int16_t *right) { + + // If the destination sample rate is higher than the OPL3 sample rate, we need to skip ahead + while (SampleAccum >= SampleRate) { + + LastOutput[0] = CurrOutput[0]; + LastOutput[1] = CurrOutput[1]; + + Output(CurrOutput[0], CurrOutput[1]); + + SampleAccum -= SampleRate; + } + + // Mix with the partial accumulation + int32_t omblend = SampleRate - SampleAccum; + *left = (LastOutput[0] * omblend + CurrOutput[0] * SampleAccum) / SampleRate; + *right = (LastOutput[1] * omblend + CurrOutput[1] * SampleAccum) / SampleRate; + + SampleAccum += OPL3SampleRate; +} + + + +//================================================================================================== +// Produce final output from the chip. This is at the OPL3 sample-rate. +//================================================================================================== +void Opal::Output(int16_t &left, int16_t &right) { + + int32_t leftmix = 0, rightmix = 0; + + // Sum the output of each channel + for (int i = 0; i < NumChannels; i++) { + + int16_t chanleft, chanright; + Chan[i].Output(chanleft, chanright); + + leftmix += chanleft; + rightmix += chanright; + } + + // Clamp + if (leftmix < -0x8000) + left = -0x8000; + else if (leftmix > 0x7FFF) + left = 0x7FFF; + else + left = leftmix; + + if (rightmix < -0x8000) + right = -0x8000; + else if (rightmix > 0x7FFF) + right = 0x7FFF; + else + right = rightmix; + + Clock++; + + // Tremolo. According to this post, the OPL3 tremolo is a 13,440 sample length triangle wave + // with a peak at 26 and a trough at 0 and is simply added to the logarithmic level accumulator + // http://forums.submarine.org.uk/phpBB/viewtopic.php?f=9&t=1171 + TremoloClock = (TremoloClock + 1) % 13440; + TremoloLevel = ((TremoloClock < 13440 / 2) ? TremoloClock : 13440 - TremoloClock) / 256; + if (!TremoloDepth) + TremoloLevel >>= 2; + + // Vibrato. This appears to be a 8 sample long triangle wave with a magnitude of the three + // high bits of the channel frequency, positive and negative, divided by two if the vibrato + // depth is zero. It is only cycled every 1,024 samples. + VibratoTick++; + if (VibratoTick >= 1024) { + VibratoTick = 0; + VibratoClock = (VibratoClock + 1) & 7; + } +} + + + +//================================================================================================== +// Channel constructor. +//================================================================================================== +Opal::Channel::Channel() { + + Master = 0; + Freq = 0; + Octave = 0; + PhaseStep = 0; + KeyScaleNumber = 0; + FeedbackShift = 0; + ModulationType = 0; + ChannelPair = 0; + Enable = true; +} + + + +//================================================================================================== +// Produce output from channel. +//================================================================================================== +void Opal::Channel::Output(int16_t &left, int16_t &right) { + + // Has the channel been disabled? This is usually a result of the 4-op enables being used to + // disable the secondary channel in each 4-op pair + if (!Enable) { + left = right = 0; + return; + } + + int16_t vibrato = (Freq >> 7) & 7; + if (!Master->VibratoDepth) + vibrato >>= 1; + + // 0 3 7 3 0 -3 -7 -3 + uint16_t clk = Master->VibratoClock; + if (!(clk & 3)) + vibrato = 0; // Position 0 and 4 is zero + else { + if (clk & 1) + vibrato >>= 1; // Odd positions are half the magnitude + if (clk & 4) + vibrato = -vibrato; // The second half positions are negative + } + + vibrato <<= Octave; + + // Combine individual operator outputs + int16_t out, acc; + + // Running in 4-op mode? + if (ChannelPair) { + + // Get the secondary channel's modulation type. This is the only thing from the secondary + // channel that is used + if (ChannelPair->GetModulationType() == 0) { + + if (ModulationType == 0) { + + // feedback -> modulator -> modulator -> modulator -> carrier + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + out = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + out = Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + + } else { + + // (feedback -> carrier) + (modulator -> modulator -> carrier) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + acc = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + acc = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + } + + } else { + + if (ModulationType == 0) { + + // (feedback -> modulator -> carrier) + (modulator -> carrier) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + acc = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + + } else { + + // (feedback -> carrier) + (modulator -> carrier) + carrier + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + acc = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + out += Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + } + } + + } else { + + // Standard 2-op mode + if (ModulationType == 0) { + + // Frequency modulation (well, phase modulation technically) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + + } else { + + // Additive + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out += Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato); + } + } + + left = LeftEnable ? out : 0; + right = RightEnable ? out : 0; +} + + + +//================================================================================================== +// Set phase step for operators using this channel. +//================================================================================================== +void Opal::Channel::SetFrequencyLow(uint16_t freq) { + + Freq = (Freq & 0x300) | (freq & 0xFF); + ComputePhaseStep(); +} +//-------------------------------------------------------------------------------------------------- +void Opal::Channel::SetFrequencyHigh(uint16_t freq) { + + Freq = (Freq & 0xFF) | ((freq & 3) << 8); + ComputePhaseStep(); + + // Only the high bits of Freq affect the Key Scale No. + ComputeKeyScaleNumber(); +} + + + +//================================================================================================== +// Set the octave of the channel (0 to 7). +//================================================================================================== +void Opal::Channel::SetOctave(uint16_t oct) { + + Octave = oct & 7; + ComputePhaseStep(); + ComputeKeyScaleNumber(); +} + + + +//================================================================================================== +// Keys the channel on/off. +//================================================================================================== +void Opal::Channel::SetKeyOn(bool on) { + + Op[0]->SetKeyOn(on); + Op[1]->SetKeyOn(on); +} + + + +//================================================================================================== +// Enable left stereo channel. +//================================================================================================== +void Opal::Channel::SetLeftEnable(bool on) { + + LeftEnable = on; +} + + + +//================================================================================================== +// Enable right stereo channel. +//================================================================================================== +void Opal::Channel::SetRightEnable(bool on) { + + RightEnable = on; +} + + + +//================================================================================================== +// Set the channel feedback amount. +//================================================================================================== +void Opal::Channel::SetFeedback(uint16_t val) { + + FeedbackShift = val ? 9 - val : 0; +} + + + +//================================================================================================== +// Set frequency modulation/additive modulation +//================================================================================================== +void Opal::Channel::SetModulationType(uint16_t type) { + + ModulationType = type; +} + + + +//================================================================================================== +// Compute the stepping factor for the operator waveform phase based on the frequency and octave +// values of the channel. +//================================================================================================== +void Opal::Channel::ComputePhaseStep() { + + PhaseStep = uint32_t(Freq) << Octave; +} + + + +//================================================================================================== +// Compute the key scale number and key scale levels. +// +// From the Yamaha data sheet this is the block/octave number as bits 3-1, with bit 0 coming from +// the MSB of the frequency if NoteSel is 1, and the 2nd MSB if NoteSel is 0. +//================================================================================================== +void Opal::Channel::ComputeKeyScaleNumber() { + + uint16_t lsb = Master->NoteSel ? Freq >> 9 : (Freq >> 8) & 1; + KeyScaleNumber = Octave << 1 | lsb; + + // Get the channel operators to recompute their rates as they're dependent on this number. They + // also need to recompute their key scale level + for (int i = 0; i < 4; i++) { + + if (!Op[i]) + continue; + + Op[i]->ComputeRates(); + Op[i]->ComputeKeyScaleLevel(); + } +} + + + +//================================================================================================== +// Operator constructor. +//================================================================================================== +Opal::Operator::Operator() { + + Master = 0; + Chan = 0; + Phase = 0; + Waveform = 0; + FreqMultTimes2 = 1; + EnvelopeStage = EnvOff; + EnvelopeLevel = 0x1FF; + AttackRate = 0; + DecayRate = 0; + SustainLevel = 0; + ReleaseRate = 0; + KeyScaleShift = 0; + KeyScaleLevel = 0; + Out[0] = Out[1] = 0; + KeyOn = false; + KeyScaleRate = false; + SustainMode = false; + TremoloEnable = false; + VibratoEnable = false; +} + + + +//================================================================================================== +// Produce output from operator. +//================================================================================================== +int16_t Opal::Operator::Output(uint16_t keyscalenum, uint32_t phase_step, int16_t vibrato, int16_t mod, int16_t fbshift) { + + // Advance wave phase + if (VibratoEnable) + phase_step += vibrato; + Phase += (phase_step * FreqMultTimes2) / 2; + + uint16_t level = (EnvelopeLevel + OutputLevel + KeyScaleLevel + (TremoloEnable ? Master->TremoloLevel : 0)) << 3; + + switch (EnvelopeStage) { + + // Attack stage + case EnvAtt: { + if (AttackRate == 0) + break; + if (AttackMask && (Master->Clock & AttackMask)) + break; + uint16_t add = ((AttackAdd >> AttackTab[Master->Clock >> AttackShift & 7]) * ~EnvelopeLevel) >> 3; + EnvelopeLevel += add; + if (EnvelopeLevel <= 0) { + EnvelopeLevel = 0; + EnvelopeStage = EnvDec; + } + break; + } + + // Decay stage + case EnvDec: { + if (DecayRate == 0) + break; + if (DecayMask && (Master->Clock & DecayMask)) + break; + uint16_t add = DecayAdd >> DecayTab[Master->Clock >> DecayShift & 7]; + EnvelopeLevel += add; + if (EnvelopeLevel >= SustainLevel) { + EnvelopeLevel = SustainLevel; + EnvelopeStage = EnvSus; + } + break; + } + + // Sustain stage + case EnvSus: { + + if (SustainMode) + break; + + // Note: fall-through! + } + + // Release stage + case EnvRel: { + if (ReleaseRate == 0) + break; + if (ReleaseMask && (Master->Clock & ReleaseMask)) + break; + uint16_t add = ReleaseAdd >> ReleaseTab[Master->Clock >> ReleaseShift & 7]; + EnvelopeLevel += add; + if (EnvelopeLevel >= 0x1FF) { + EnvelopeLevel = 0x1FF; + EnvelopeStage = EnvOff; + Out[0] = Out[1] = 0; + return 0; + } + break; + } + + // Envelope, and therefore the operator, is not running + default: + Out[0] = Out[1] = 0; + return 0; + } + + // Feedback? In that case we modulate by a blend of the last two samples + if (fbshift) + mod += (Out[0] + Out[1]) >> fbshift; + + uint16_t phase = (Phase >> 10) + mod; + uint16_t offset = phase & 0xFF; + uint16_t logsin; + bool negate = false; + + switch (Waveform) { + + //------------------------------------ + // Standard sine wave + //------------------------------------ + case 0: + if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + negate = (phase & 0x200); + break; + + //------------------------------------ + // Half sine wave + //------------------------------------ + case 1: + if (phase & 0x200) + offset = 0; + else if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Positive sine wave + //------------------------------------ + case 2: + if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Quarter positive sine wave + //------------------------------------ + case 3: + if (phase & 0x100) + offset = 0; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Double-speed sine wave + //------------------------------------ + case 4: + if (phase & 0x200) + offset = 0; + + else { + + if (phase & 0x80) + offset ^= 0xFF; + + offset = (offset + offset) & 0xFF; + negate = (phase & 0x100); + } + + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Double-speed positive sine wave + //------------------------------------ + case 5: + if (phase & 0x200) + offset = 0; + + else { + + offset = (offset + offset) & 0xFF; + if (phase & 0x80) + offset ^= 0xFF; + } + + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Square wave + //------------------------------------ + case 6: + logsin = 0; + negate = (phase & 0x200); + break; + + //------------------------------------ + // Exponentiation wave + //------------------------------------ + default: + logsin = phase & 0x1FF; + if (phase & 0x200) { + logsin ^= 0x1FF; + negate = true; + } + logsin <<= 3; + break; + } + + uint16_t mix = logsin + level; + if (mix > 0x1FFF) + mix = 0x1FFF; + + // From the OPLx decapsulated docs: + // "When such a table is used for calculation of the exponential, the table is read at the + // position given by the 8 LSB's of the input. The value + 1024 (the hidden bit) is then the + // significand of the floating point output and the yet unused MSB's of the input are the + // exponent of the floating point output." + int16_t v = Master->ExpTable[mix & 0xFF] + 1024; + v >>= mix >> 8; + v += v; + if (negate) + v = ~v; + + // Keep last two results for feedback calculation + Out[1] = Out[0]; + Out[0] = v; + + return v; +} + + + +//================================================================================================== +// Trigger operator. +//================================================================================================== +void Opal::Operator::SetKeyOn(bool on) { + + // Already on/off? + if (KeyOn == on) + return; + KeyOn = on; + + if (on) { + + // The highest attack rate is instant; it bypasses the attack phase + if (AttackRate == 15) { + EnvelopeStage = EnvDec; + EnvelopeLevel = 0; + } else + EnvelopeStage = EnvAtt; + + Phase = 0; + + } else { + + // Stopping current sound? + if (EnvelopeStage != EnvOff && EnvelopeStage != EnvRel) + EnvelopeStage = EnvRel; + } +} + + + +//================================================================================================== +// Enable amplitude vibrato. +//================================================================================================== +void Opal::Operator::SetTremoloEnable(bool on) { + + TremoloEnable = on; +} + + + +//================================================================================================== +// Enable frequency vibrato. +//================================================================================================== +void Opal::Operator::SetVibratoEnable(bool on) { + + VibratoEnable = on; +} + + + +//================================================================================================== +// Sets whether we release or sustain during the sustain phase of the envelope. 'true' is to +// sustain, otherwise release. +//================================================================================================== +void Opal::Operator::SetSustainMode(bool on) { + + SustainMode = on; +} + + + +//================================================================================================== +// Key scale rate. Sets how much the Key Scaling Number affects the envelope rates. +//================================================================================================== +void Opal::Operator::SetEnvelopeScaling(bool on) { + + KeyScaleRate = on; + ComputeRates(); +} + + + +//================================================================================================== +// Multiplies the phase frequency. +//================================================================================================== +void Opal::Operator::SetFrequencyMultiplier(uint16_t scale) { + + // Needs to be multiplied by two (and divided by two later when we use it) because the first + // entry is actually .5 + const uint16_t mul_times_2[] = { + 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30, + }; + + FreqMultTimes2 = mul_times_2[scale & 15]; +} + + + +//================================================================================================== +// Attenuates output level towards higher pitch. +//================================================================================================== +void Opal::Operator::SetKeyScale(uint16_t scale) { + + if (scale > 0) + KeyScaleShift = 3 - scale; + + // No scaling, ensure it has no effect + else + KeyScaleShift = 15; + + ComputeKeyScaleLevel(); +} + + + +//================================================================================================== +// Sets the output level (volume) of the operator. +//================================================================================================== +void Opal::Operator::SetOutputLevel(uint16_t level) { + + OutputLevel = level * 4; +} + + + +//================================================================================================== +// Operator attack rate. +//================================================================================================== +void Opal::Operator::SetAttackRate(uint16_t rate) { + + AttackRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Operator decay rate. +//================================================================================================== +void Opal::Operator::SetDecayRate(uint16_t rate) { + + DecayRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Operator sustain level. +//================================================================================================== +void Opal::Operator::SetSustainLevel(uint16_t level) { + + SustainLevel = level < 15 ? level : 31; + SustainLevel *= 16; +} + + + +//================================================================================================== +// Operator release rate. +//================================================================================================== +void Opal::Operator::SetReleaseRate(uint16_t rate) { + + ReleaseRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Assign the waveform this operator will use. +//================================================================================================== +void Opal::Operator::SetWaveform(uint16_t wave) { + + Waveform = wave & 7; +} + + + +//================================================================================================== +// Compute actual rate from register rate. From the Yamaha data sheet: +// +// Actual rate = Rate value * 4 + Rof, if Rate value = 0, actual rate = 0 +// +// Rof is set as follows depending on the KSR setting: +// +// Key scale 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// KSR = 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 +// KSR = 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// +// Note: zero rates are infinite, and are treated separately elsewhere +//================================================================================================== +void Opal::Operator::ComputeRates() { + + int combined_rate = AttackRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + int rate_high = combined_rate >> 2; + int rate_low = combined_rate & 3; + + AttackShift = rate_high < 12 ? 12 - rate_high : 0; + AttackMask = (1 << AttackShift) - 1; + AttackAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + AttackTab = Master->RateTables[rate_low]; + + // Attack rate of 15 is always instant + if (AttackRate == 15) + AttackAdd = 0xFFF; + + combined_rate = DecayRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + rate_high = combined_rate >> 2; + rate_low = combined_rate & 3; + + DecayShift = rate_high < 12 ? 12 - rate_high : 0; + DecayMask = (1 << DecayShift) - 1; + DecayAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + DecayTab = Master->RateTables[rate_low]; + + combined_rate = ReleaseRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + rate_high = combined_rate >> 2; + rate_low = combined_rate & 3; + + ReleaseShift = rate_high < 12 ? 12 - rate_high : 0; + ReleaseMask = (1 << ReleaseShift) - 1; + ReleaseAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + ReleaseTab = Master->RateTables[rate_low]; +} + + + +//================================================================================================== +// Compute the operator's key scale level. This changes based on the channel frequency/octave and +// operator key scale value. +//================================================================================================== +void Opal::Operator::ComputeKeyScaleLevel() { + + static const uint16_t levtab[] = { + 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, 8, 12, 16, 20, 24, 28, 32, + 0, 0, 0, 0, 0, 12, 20, 28, 32, 40, 44, 48, 52, 56, 60, 64, + 0, 0, 0, 20, 32, 44, 52, 60, 64, 72, 76, 80, 84, 88, 92, 96, + 0, 0, 32, 52, 64, 76, 84, 92, 96, 104, 108, 112, 116, 120, 124, 128, + 0, 32, 64, 84, 96, 108, 116, 124, 128, 136, 140, 144, 148, 152, 156, 160, + 0, 64, 96, 116, 128, 140, 148, 156, 160, 168, 172, 176, 180, 184, 188, 192, + 0, 96, 128, 148, 160, 172, 180, 188, 192, 200, 204, 208, 212, 216, 220, 224, + }; + + // This uses a combined value of the top four bits of frequency with the octave/block + uint16_t i = (Chan->GetOctave() << 4) | (Chan->GetFreq() >> 6); + KeyScaleLevel = levtab[i] >> KeyScaleShift; +} + +Opal opal(44100); + +std::mutex mutex; + +namespace Opl { + void write(uint16_t reg, uint8_t data) { + mutex.lock(); + opal.Port(reg, data); + mutex.unlock(); + } + + void generateSample(int16_t *left, int16_t *right) { + mutex.lock(); + opal.Sample(left, right); + mutex.unlock(); + } +} diff --git a/system/sdl/SoundBlaster.cpp b/system/sdl/SoundBlaster.cpp new file mode 100644 index 0000000..36bcffa --- /dev/null +++ b/system/sdl/SoundBlaster.cpp @@ -0,0 +1,19 @@ +#include "../SoundBlaster.h" + +SoundBlaster soundblaster; + +void soundblasterIsr() { + soundblaster.onInterrupt(); +} + +SoundBlaster::SoundBlaster() { + +} + +SoundBlaster::~SoundBlaster() { +} + +void SoundBlaster::onInterrupt() { + +} + diff --git a/system/sdl/Timer.cpp b/system/sdl/Timer.cpp new file mode 100644 index 0000000..596aa6a --- /dev/null +++ b/system/sdl/Timer.cpp @@ -0,0 +1,55 @@ +#include + +#include "../Timer.h" + +SDL_TimerID timerId; + +void timerISR() { + Timer::instance().update(); +} + +Timer::Timer() { + _freq = 50; + timerId = SDL_AddTimer(1000/_freq, [] (Uint32, void*) -> Uint32 { + timerISR(); + return 1000 / instance().getFrequency(); + }, nullptr); +} + +Timer::~Timer() { + SDL_RemoveTimer(timerId); +} + +void Timer::update() { + // TODO + _ticks++; + if (_callback) _callback(); +} + +uint32_t Timer::getTicks() const { + return _ticks; +} + +void Timer::setFrequency(uint16_t freq) { + _freq = freq; + // TODO +} + +uint16_t Timer::getFrequency() const { + return _freq; +} + +void Timer::setDivider(uint16_t div) { + // TODO +} + +Timer::Callback Timer::setCallback(Timer::Callback callback) { + auto oldCallback = _callback; + _callback = callback; + return oldCallback; +} + +Timer &Timer::instance() { + static Timer inst; + return inst; +} diff --git a/system/sdl/Video.cpp b/system/sdl/Video.cpp new file mode 100644 index 0000000..3eceab3 --- /dev/null +++ b/system/sdl/Video.cpp @@ -0,0 +1,125 @@ +#include + +#include "../Video.h" + +#include "Events.h" + +#define WINDOW_WIDTH 1280 +#define WINDOW_HEIGHT 800 + +Video video; + +Video::Video() { + // TODO +} + +Video::~Video() { + exit(); +} + +inline void writePixel(SDL_Surface *surface, unsigned x, unsigned y, Uint8 r, Uint8 g, Uint8 b, Uint8 a) { + auto row = reinterpret_cast(static_cast(surface->pixels) + y * surface->pitch); + row[x] = SDL_MapRGBA(surface->format, r, g, b, a); +} + +void Video::enter() { + _window = SDL_CreateWindow("game", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 800, SDL_WINDOW_SHOWN); + SDL_SetWindowTitle(_window, "meow :3"); + if (!_window) { + std::cerr << "Error creating SDL window: " << SDL_GetError() << std::endl; + abort(); + } + + _windowSurface = SDL_GetWindowSurface(_window); + _fb = SDL_CreateRGBSurfaceWithFormat(0, SCREEN_WIDTH, SCREEN_HEIGHT, 32, SDL_PIXELFORMAT_ARGB8888); +} + +void Video::exit() { + if (_fb) SDL_FreeSurface(_fb); + _fb = nullptr; + + SDL_DestroyWindow(_window); +} + +void Video::SetMode(uint8_t mode) { + // Ignored +} + +uint8_t Video::GetMode() { + // Fixed + return 0x13; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry entry) { + SetPaletteEntry(index, &entry); +} + +Video::PaletteEntry Video::GetPaletteEntry(uint8_t index) { + PaletteEntry entry; + GetPaletteEntry(index, &entry); + return entry; +} + +void Video::SetPaletteEntry(uint8_t index, PaletteEntry *entry) { + _palette[index] = *entry; +} + +void Video::GetPaletteEntry(uint8_t index, PaletteEntry *entry) { + *entry = _palette[index]; +} + +void *Video::GetFB() { + return _renderBuffer; +} + +void Video::WaitForVerticalSync() { + auto targetTime = _lastUpdate + std::chrono::microseconds(14286); + auto currentTime = std::chrono::steady_clock::now(); + + while (currentTime < targetTime) { + currentTime = std::chrono::steady_clock::now(); + } + _lastUpdate = currentTime; +} + +void Video::UpdateRect(const Rect &rect) { + // Merge rect if it overlaps with an existing rect + for (auto i = 0; i < _updateRectIndex; i++) { + auto &r = _updatedRects[i]; + if (r.overlaps(rect)) { + r += rect; + return; + } + } + + // Add a new rect to the list + _updatedRects[_updateRectIndex++] = rect; +} + +void Video::Flip() { + SDL_LockSurface(_fb); + + for (auto i = 0; i <= MAX_UPDATE_RECT_INDEX; i++) { + auto &rect = _updatedRects[i]; + rect.clamp(0, 0, _fb->w, _fb->h); + + for (auto y = rect.y1; y < rect.y2; y++) { + for (auto x = rect.x1; x < rect.x2; x++) { + auto &paletteEntry = _palette[_renderBuffer[y * SCREEN_WIDTH + x]]; + + writePixel(_fb, x, y, paletteEntry.r, paletteEntry.g, paletteEntry.b, 255); + } + } + } + SDL_UnlockSurface(_fb); + + SDL_Rect srcRect{0, 0, SCREEN_WIDTH, SCREEN_HEIGHT}; + SDL_Rect dstRect{0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}; + + SDL_BlitScaled(_fb, &srcRect, _windowSurface, &dstRect); + SDL_UpdateWindowSurface(_window); + + _updateRectIndex = 0; + + events.poll(); +} diff --git a/system/sdl/init.cpp b/system/sdl/init.cpp new file mode 100644 index 0000000..4aa0d0b --- /dev/null +++ b/system/sdl/init.cpp @@ -0,0 +1,25 @@ +#include "../init.h" + +#include +#include + +#include "AudioBackend.h" +#include "../Video.h" + +namespace System { + void init() { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) { + std::cerr << "Error initializing SDL: " << SDL_GetError() << std::endl; + abort(); + } + + audioBackend.init(); + } + + void terminate() { + audioBackend.terminate(); + video.exit(); + SDL_Quit(); + } +} + diff --git a/util/Asm.h b/util/Asm.h new file mode 100644 index 0000000..cff9507 --- /dev/null +++ b/util/Asm.h @@ -0,0 +1,68 @@ +#ifndef ASM_H +#define ASM_H + +#include + +static inline uint8_t inb(uint16_t port) { + uint8_t value; + asm volatile ("inb %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline uint16_t inw(uint16_t port) { + uint16_t value; + asm volatile ("inw %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline uint32_t inl(uint16_t port) { + uint32_t value; + asm volatile ("inl %1, %0" : "=a"(value) : "Nd"(port)); + return value; +} + +static inline void outb(uint16_t port, uint8_t value) { + asm volatile ("outb %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline void outw(uint16_t port, uint16_t value) { + asm volatile ("outw %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline void outl(uint16_t port, uint32_t value) { + asm volatile ("outl %0, %1" : : "a"(value), "Nd"(port)); +} + +static inline void enableInterrupts() { + asm volatile ("sti": : :"memory"); +} + +static inline void disableInterrupts() { + asm volatile ("cli": : :"memory"); +} + +static inline uint32_t eflags() { + uint32_t value; + asm volatile ( "pushf\n\t" + "popl %0" + : "=g"(value)); + return value; +} + +static inline bool interruptsEnabled() { + return (eflags() & (1u << 9u)) != 0; +} + +static void pmInterruptCall(unsigned short selector, unsigned long offset) { + __asm__ __volatile__ ( + "pushf\n" // Push EFLAGS + "mov %0, %%ax\n" // Load the segment selector into AX + "mov %%ax, %%ds\n" // Move it to DS (or another segment register) + "call *%1\n" // Perform the indirect far call using the offset + : // No output operands + : "r" (selector), "r" (offset) // Input operands + : "%ax" // Clobbered register + ); +} + +#endif \ No newline at end of file diff --git a/util/Bmp.cpp b/util/Bmp.cpp new file mode 100644 index 0000000..8150b85 --- /dev/null +++ b/util/Bmp.cpp @@ -0,0 +1,97 @@ +#include "Bmp.h" + +#include + +#include "Files.h" + +namespace Bmp { + Bitmap loadFromMemory(void *buf, size_t len) { + if (len < sizeof (BmpFileHeader) + sizeof (BmpInfoHeader)) return {}; + + auto fileHeader = reinterpret_cast(buf); + auto infoHeader = reinterpret_cast(fileHeader + 1); + + if (fileHeader->signature != 0x4d42) { + printf("Invalid signature\n"); + return {}; + } + if (len < fileHeader->fileSize) { + printf("File too small\n"); + return {}; + } + if (infoHeader->headerSize != 40) { + printf("Invalid header size\n"); + return {}; + } + if (infoHeader->width < 0 || infoHeader->height < 0 || infoHeader->planes != 1) { + printf("Invalid width/height/bit planes\n"); + + return {}; + } + if (infoHeader->bitsPerPixel != 1 && infoHeader->bitsPerPixel != 4 && infoHeader->bitsPerPixel != 8) { + printf("Unsupported bpp %u\n", infoHeader->bitsPerPixel); + + return {}; + } + if (infoHeader->compression != BmpInfoHeader::CompressionType::Rgb) { + printf("Unsupported compression\n"); + + return {}; + } + + // TODO check imageSize + + Bitmap bitmap(infoHeader->width, infoHeader->height); + + // Read color table + auto colorTable = reinterpret_cast(infoHeader + 1); + for (auto i = 0; i < infoHeader->colorsUsed; i++) { + Video::PaletteEntry paletteEntry; + paletteEntry.b = colorTable[4 * i + 0]; + paletteEntry.g = colorTable[4 * i + 1]; + paletteEntry.r = colorTable[4 * i + 2]; + bitmap.SetPaletteEntry(i, paletteEntry); + } + + // Read pixel data + auto data = static_cast(buf) + fileHeader->offset; + auto rowLength = infoHeader->width; + if (infoHeader->bitsPerPixel == 1) rowLength = (rowLength + 7) >> 3; + else if (infoHeader->bitsPerPixel == 4) rowLength = (rowLength + 1) >> 1; + auto stride = (rowLength + 3) >> 2 << 2; + for (auto y = 0u; y < infoHeader->height; y++) { + for (auto x = 0u; x < infoHeader->width; x++) { + uint8_t pv = 0; + switch (infoHeader->bitsPerPixel) { + case 1: { + const auto byte = data[(infoHeader->height - y - 1) * stride + (x >> 3)]; + auto pixelOffset = x & 7; + pv = (byte >> (7 - pixelOffset)) & 1; + break; + } + case 4: { + const auto byte = data[(infoHeader->height - y - 1) * stride + (x >> 1)]; + pv = x & 1 ? byte & 0xf : byte >> 4; + break; + } + case 8: { + pv = data[(infoHeader->height - y - 1) * stride + x]; + break; + } + } + bitmap.SetPixel(x, y, pv); + } + } + + return bitmap; + } + + Bitmap loadFromFile(const char* path) { + void *data; + auto len = Files::allocateBufferAndLoadFromFile(path, &data); + auto bitmap = loadFromMemory(data, len); + Files::deleteBuffer(data); + + return bitmap; + } +} diff --git a/util/Bmp.h b/util/Bmp.h new file mode 100644 index 0000000..e0afd2a --- /dev/null +++ b/util/Bmp.h @@ -0,0 +1,48 @@ +#ifndef GAME_BMPLOADER_H +#define GAME_BMPLOADER_H + +#include +#include "../graphics/Bitmap.h" + +namespace Bmp { + struct BmpFileHeader { + uint16_t signature; + uint32_t fileSize; + uint16_t reserved1; + uint16_t reserved2; + uint32_t offset; + } __attribute__((__packed__)); + + struct BmpInfoHeader { + uint32_t headerSize; + uint32_t width; + uint32_t height; + uint16_t planes; + uint16_t bitsPerPixel; + uint32_t compression; + uint32_t imageSize; + uint32_t horizontalResolution; + uint32_t verticalResolution; + uint32_t colorsUsed; + uint32_t colorsImportant; + + enum CompressionType { + Rgb = 0, + Rle8, + Rle4, + Bitfields, + Jpeg, + Png, + Alphabitfields, + Cmyk, + CmykRle8, + CmykRle4, + }; + } __attribute__((__packed__)); + + Bitmap loadFromMemory(void *buf, size_t len); + Bitmap loadFromFile(const char* path); +}; + + +#endif //GAME_BMPLOADER_H \ No newline at end of file diff --git a/util/Files.cpp b/util/Files.cpp new file mode 100644 index 0000000..18d9a1f --- /dev/null +++ b/util/Files.cpp @@ -0,0 +1,27 @@ +#include "Files.h" + +#include + +size_t Files::allocateBufferAndLoadFromFile(const char *path, void **bufferPointer) { + auto fp = fopen(path, "rb"); + if (!fp) { + *bufferPointer = nullptr; + return 0; + } + + fseek(fp, 0, SEEK_END); + size_t len = ftell(fp); + fseek(fp, 0, SEEK_SET); + + auto data = new unsigned char[len]; + fread(data, 1, len, fp); + fclose(fp); + + *bufferPointer = data; + return len; +} + +void Files::deleteBuffer(void *buffer) { + delete[] static_cast(buffer); +} + diff --git a/util/Files.h b/util/Files.h new file mode 100644 index 0000000..e94f176 --- /dev/null +++ b/util/Files.h @@ -0,0 +1,10 @@ +#ifndef GAME_FILES_H +#define GAME_FILES_H +#include + +namespace Files { + size_t allocateBufferAndLoadFromFile(const char *path, void **bufferPointer); + void deleteBuffer(void *); +} + +#endif //GAME_FILES_H \ No newline at end of file diff --git a/util/Gbm.cpp b/util/Gbm.cpp new file mode 100644 index 0000000..cdf50b4 --- /dev/null +++ b/util/Gbm.cpp @@ -0,0 +1,68 @@ +#include "Gbm.h" + +#include + +#include "Files.h" + +#define GBM_SIGNATURE 0x204d4247 + +Bitmap Gbm::loadFromMemory(const void *data, size_t size) { + // Check for minimum + if (size < sizeof (uint32_t) + sizeof (uint16_t) + sizeof (uint16_t) + 16 * 3) return {}; + + auto gbm = static_cast(data); + + if (gbm->signature != GBM_SIGNATURE) return {}; + if (gbm->width == 0 || gbm->height == 0) return {}; + + Bitmap bitmap(gbm->width, gbm->height); + for (auto i = 0; i < 16; i++) { + bitmap.SetPaletteEntry(i, {gbm->palette[i].r, gbm->palette[i].g, gbm->palette[i].b}); + } + + for (auto y = 0; y < gbm->height; y++) { + for (auto x = 0; x < gbm->width; x++) { + auto pv = gbm->data[y * gbm->width + x]; + bitmap.SetPixel(x, y, pv); + } + } + + return bitmap; +} + +Bitmap Gbm::loadFromFile(const char *path) { + void *data; + auto len = Files::allocateBufferAndLoadFromFile(path, &data); + auto bitmap = loadFromMemory(data, len); + Files::deleteBuffer(data); + + return bitmap; +} + +void Gbm::writeToFile(const char *path, const Bitmap &bitmap) { + auto fp = fopen(path, "wb"); + if (!fp) return; + + // Write signature + uint32_t signature = GBM_SIGNATURE; + fwrite(&signature, 4, 1, fp); + uint16_t width = bitmap.GetWidth(), height = bitmap.GetHeight(); + fwrite(&width, 2, 1, fp); + fwrite(&height, 2, 1, fp); + for (auto i = 0; i < 16; i++) { + auto entry = bitmap.GetPaletteEntry(i); + auto r = entry.r, g = entry.g, b = entry.b; + fwrite(&r, 1, 1, fp); + fwrite(&g, 1, 1, fp); + fwrite(&b, 1, 1, fp); + } + + for (auto y = 0; y < height; y++) { + for (auto x = 0; x < width; x++) { + auto pv = bitmap.GetPixel(x, y); + fwrite(&pv, 1, 1, fp); + } + } + + fclose(fp); +} diff --git a/util/Gbm.h b/util/Gbm.h new file mode 100644 index 0000000..c49bac8 --- /dev/null +++ b/util/Gbm.h @@ -0,0 +1,28 @@ +#ifndef GAME_GBM_H +#define GAME_GBM_H + +#include + +#include "../graphics/Bitmap.h" + +namespace Gbm { + struct GbmHeader { + uint32_t signature; + uint16_t width; + uint16_t height; + struct { + uint8_t r; + uint8_t g; + uint8_t b; + } __attribute__((__packed__)) palette[16]; + uint8_t data[0]; + } __attribute__((__packed__)); + + Bitmap loadFromMemory(const void *data, size_t size); + Bitmap loadFromFile(const char *path); + + void writeToFile(const char *path, const Bitmap &bitmap); +}; + + +#endif //GAME_GBM_H \ No newline at end of file diff --git a/util/Log.cpp b/util/Log.cpp new file mode 100644 index 0000000..4694c99 --- /dev/null +++ b/util/Log.cpp @@ -0,0 +1,22 @@ +#include "Log.h" + +#include + +Log DefaultLog("log.txt"); + +Log::Log(const char *path) { + fp = fopen(path, "w"); +} + +Log::~Log() { + fclose(fp); +} + +void Log::log(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(fp, fmt, args); + va_end(args); + + fflush(fp); +} diff --git a/util/Log.h b/util/Log.h new file mode 100644 index 0000000..09a58fc --- /dev/null +++ b/util/Log.h @@ -0,0 +1,19 @@ +#ifndef GAME_LOG_H +#define GAME_LOG_H +#include + +class Log { +public: + Log(const char *path); + ~Log(); + + void log(const char *fmt, ...); + +private: + FILE *fp; +}; + +extern Log DefaultLog; + + +#endif //GAME_LOG_H \ No newline at end of file