commit 7024bfd19442df4cb29a624d96fbe8f3653f37da Author: Vanessa Date: Mon Oct 13 15:12:35 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5b04b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +*.o +*.gbm +gbmconv +game.exe +game.img diff --git a/CWSDPMI.EXE b/CWSDPMI.EXE new file mode 100644 index 0000000..6865892 Binary files /dev/null and b/CWSDPMI.EXE differ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..796056f --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +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 + +CFLAGS=-O3 -march=i486 +CXXFLAGS=$(CFLAGS) +LDFLAGS= + +EXE=game.exe +OBJ=main.o +OBJ+=system/Timer.o system/Video.o system/Keyboard.o +OBJ+=graphics/Bitmap.o +OBJ+=audio/rad20player.o audio/AudioPlayer.o audio/Music.o audio/Audio.o +OBJ+=util/Log.o util/Files.o util/Bmp.o util/Gbm.o +OBJ+=scenes/Scene.o scenes/IntroScene.o scenes/MainMenuScene.o scenes/GameScene.o + +CONVERTER_CXX=g++ +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 +EXTRA_FILES=install.bat +DPMI_HOST=CWSDPMI.EXE + +RELEASE_FILES=$(EXE) $(DPMI_HOST) $(GFX_ASSETS) $(MUSIC_ASSETS) $(EXTRA_FILES) + +FLOPPY_IMG=game.img + +.PHONY: all clean release assets floppy + +all: $(CONVERTER_EXE) $(EXE) + +clean: ; rm -rf $(OBJ) $(EXE) $(CONVERTER_EXE) $(FLOPPY_IMG) $(COMPILED_GFX_ASSETS) release + +release: all assets; $(STRIP) $(EXE) && upx $(EXE); mkdir release; cp -Rv $(RELEASE_FILES) release/ + +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) + +%.o : %.c ; $(CC) $(CFLAGS) -c $< -o $@ + +%.o : %.cpp ; $(CXX) $(CXXFLAGS) -c $< -o $@ + +assets: $(CONVERTER_EXE) $(GFX_ASSETS) + +%.gbm : %.bmp %_m.bmp ; ./$(CONVERTER_EXE) $@ $^ diff --git a/assets/bg.bmp b/assets/bg.bmp new file mode 100644 index 0000000..62fa6b4 Binary files /dev/null and b/assets/bg.bmp differ diff --git a/assets/cow.bmp b/assets/cow.bmp new file mode 100644 index 0000000..65050a5 Binary files /dev/null and b/assets/cow.bmp differ diff --git a/assets/cow.xcf b/assets/cow.xcf new file mode 100644 index 0000000..e63a063 Binary files /dev/null and b/assets/cow.xcf differ diff --git a/assets/cow_m.bmp b/assets/cow_m.bmp new file mode 100644 index 0000000..9f52e24 Binary files /dev/null and b/assets/cow_m.bmp differ diff --git a/assets/flaunch.pcm b/assets/flaunch.pcm new file mode 100644 index 0000000..a6b15c6 Binary files /dev/null and b/assets/flaunch.pcm differ diff --git a/assets/font1.bmp b/assets/font1.bmp new file mode 100644 index 0000000..04af59e Binary files /dev/null and b/assets/font1.bmp differ diff --git a/assets/font1_m.bmp b/assets/font1_m.bmp new file mode 100644 index 0000000..04af59e Binary files /dev/null and b/assets/font1_m.bmp differ diff --git a/assets/getup.rad b/assets/getup.rad new file mode 100644 index 0000000..ac804e1 Binary files /dev/null and b/assets/getup.rad differ diff --git a/assets/rain.rad b/assets/rain.rad new file mode 100644 index 0000000..00e42c2 Binary files /dev/null and b/assets/rain.rad differ diff --git a/assets/spiral.rad b/assets/spiral.rad new file mode 100644 index 0000000..888bac6 Binary files /dev/null and b/assets/spiral.rad differ diff --git a/assets/witch.bmp b/assets/witch.bmp new file mode 100644 index 0000000..67b72ea Binary files /dev/null and b/assets/witch.bmp differ diff --git a/assets/witch.xcf b/assets/witch.xcf new file mode 100644 index 0000000..ae2243f Binary files /dev/null and b/assets/witch.xcf differ diff --git a/assets/witch_m.bmp b/assets/witch_m.bmp new file mode 100644 index 0000000..b770be9 Binary files /dev/null and b/assets/witch_m.bmp differ diff --git a/audio/Audio.cpp b/audio/Audio.cpp new file mode 100644 index 0000000..bc6787e --- /dev/null +++ b/audio/Audio.cpp @@ -0,0 +1,35 @@ +#include "Audio.h" +#include "../util/Files.h" + +#include "AudioPlayer.h" + +Audio::Audio() { +} + +Audio::Audio(const char *path) { + loadFromFile(path); +} + +Audio::~Audio() { + unload(); +} + +bool Audio::isValid() const { + return _data != nullptr; +} + +void Audio::loadFromFile(const char *path) { + unload(); + _length = Files::allocateBufferAndLoadFromFile(path, &_data); +} + +void Audio::unload() { + if (_data) { + if (audioPlayer.isPlaying(*this)) { + audioPlayer.stopAudio(*this); + } + Files::deleteBuffer(_data); + _length = 0; + } +} + diff --git a/audio/Audio.h b/audio/Audio.h new file mode 100644 index 0000000..e7fa7ac --- /dev/null +++ b/audio/Audio.h @@ -0,0 +1,22 @@ +#ifndef GAME_AUDIO_H +#define GAME_AUDIO_H + +#include + +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..d4626ea --- /dev/null +++ b/audio/AudioPlayer.cpp @@ -0,0 +1,149 @@ +#include + +#include "AudioPlayer.h" + +#include "../system/Timer.h" +#include "../util/Asm.h" + +#define DMA_BUFFER_SIZE (SB_BUFFER_SIZE * 4) + +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(); +} + +AudioPlayer::AudioPlayer() { + // Allocate twice the amount needed and select one half depending on whether it crosses a 64k boundary + _dmaBuffer.size = (DMA_BUFFER_SIZE + 15) >> (4-1); + _go32_dpmi_allocate_dos_memory(&_dmaBuffer); + + auto physFirst = (_dmaBuffer.rm_segment << 4) + _dmaBuffer.rm_offset; + auto physSecond = physFirst + 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; + } + + timer.setCallback(audioPlayerOnTimer); +} + +AudioPlayer::~AudioPlayer() { + timer.setCallback(nullptr); + _go32_dpmi_free_dos_memory(&_dmaBuffer); +} + +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.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; + } +} + +bool AudioPlayer::isPlaying(const Music &music) const { + return _currentMusic == &music; +} + +void AudioPlayer::writeOpl(uint16_t reg, uint8_t data) const { + if (reg >= 0x100) { + outb(_oplReg + 2, reg & 0xff); + outb(_oplReg + 3, data); + } else { + outb(_oplReg, reg); + outb(_oplReg + 1, data); + } +} + +void AudioPlayer::onTimer() { + if (_paused) return; + if (!_currentMusic) return; + + _musicPlayer.Update(); +} + +void AudioPlayer::generateSamples() { + +} + +void AudioPlayer::copySamples() { + // No data available - should not happen, but let's force generate a block + if (_nextBufferReadIndex == _nextBufferWriteIndex) { + generateSamples(); + } + + dosmemput(_buffers, SB_BUFFER_SIZE, (_dmaBuffer.rm_segment << 4) + _nextBufferReadIndex * SB_BUFFER_SIZE); + _nextBufferReadIndex = (_nextBufferReadIndex + 1) & 3; +} + diff --git a/audio/AudioPlayer.h b/audio/AudioPlayer.h new file mode 100644 index 0000000..0585c85 --- /dev/null +++ b/audio/AudioPlayer.h @@ -0,0 +1,74 @@ +#ifndef GAME_AUDIOPLAYER_H +#define GAME_AUDIOPLAYER_H + +#include + +#include "Audio.h" +#include "Music.h" +#include "rad20player.h" + +#define SB_BUFFER_SIZE 1024 +#define SB_BUFFERS 32 + +#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(); + + +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}; + uint16_t _oplReg{0x388}; + _go32_dpmi_seginfo _dmaBuffer{}; + uint8_t (*_buffers)[SB_BUFFER_SIZE]{}; + uint8_t _dmaPage{0}; + uint16_t _dmaOffset{0}; + unsigned _nextBufferReadIndex{0}; + unsigned _nextBufferWriteIndex{0}; + + 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..05fc355 --- /dev/null +++ b/main.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include + +#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/Keyboard.h" +#include "util/Files.h" +#include "util/Gbm.h" +#include "util/Log.h" + +volatile bool showFps = true; +volatile bool running = true; + +uint8_t *loadMusicFile(const char *filename) { + void *data = nullptr; + Files::allocateBufferAndLoadFromFile(filename, &data); + return static_cast(data); +} + +int main() { + __djgpp_nearptr_enable(); + 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[3]{'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; + break; + default: + // Do nothing + break; + } + }); + keyboard.setKeyRepeatHandler([](unsigned char key) { + DefaultLog.log("KeyRepeat %u (%s)\n", key, keyboard.getKeyName(key)); + }); + + while (running) { + auto scene = Scenes::getCurrentScene(); + + scene->run(); + + // Render fps + if (showFps) { + ++frameCount; + auto ticks = timer.getTicks(); + if (ticks > fpsTick + 50) { + fpsTick = ticks; + fps = frameCount; + frameCount = 0; + fpsDigits[0] = '0' + (fps / 10) % 10; + fpsDigits[1] = '0' + fps % 10; + } + + 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); + + video.UpdateRect({0, 0, 14, 9 }); + } + + // Wait for vertical retrace and then copy render buffer to screen + video.WaitForVerticalSync(); + video.Flip(); + } + + audioPlayer.stopMusic(); +} 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.cpp b/system/Keyboard.cpp new file mode 100644 index 0000000..6de9829 --- /dev/null +++ b/system/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/Keyboard.h b/system/Keyboard.h new file mode 100644 index 0000000..974bc4d --- /dev/null +++ b/system/Keyboard.h @@ -0,0 +1,176 @@ +#ifndef GAME_KEYBOARD_H +#define GAME_KEYBOARD_H + +#include + +#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); + + _go32_dpmi_seginfo _oldIsr, _newIsr; +}; + +extern Keyboard keyboard; + +#endif //GAME_KEYBOARD_H \ No newline at end of file diff --git a/system/Pic.h b/system/Pic.h new file mode 100644 index 0000000..f828132 --- /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 inline void clearInterrupt() { + outb(0x20, 0x20); + } +} + +#endif //GAME_PIC_H \ No newline at end of file diff --git a/system/Timer.cpp b/system/Timer.cpp new file mode 100644 index 0000000..0310773 --- /dev/null +++ b/system/Timer.cpp @@ -0,0 +1,92 @@ +#include + +#include "Timer.h" + +#include "Pic.h" +#include "../util/Asm.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 + +Timer timer; + +void timerISR() { + timer.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) _callback(); + + _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; +} + +void Timer::setFrequency(uint16_t 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 (1) enableInterrupts(); +} + +Timer::Callback Timer::setCallback(Timer::Callback callback) { + auto oldCallback = _callback; + _callback = callback; + return oldCallback; +} diff --git a/system/Timer.h b/system/Timer.h new file mode 100644 index 0000000..dc249d4 --- /dev/null +++ b/system/Timer.h @@ -0,0 +1,38 @@ +#ifndef TICKS_H +#define TICKS_H + +#include +#include + +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; + +private: + friend void timerISR(); + + _go32_dpmi_seginfo _oldIsr{}, _newIsr{}; + uint32_t _ticks{0}; + uint32_t _elapsed{0}; + uint16_t _div{0}; + + void (*_callback)(){nullptr}; + + void update(); +}; + +extern Timer timer; + +#endif diff --git a/system/Video.cpp b/system/Video.cpp new file mode 100644 index 0000000..ba0db13 --- /dev/null +++ b/system/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/Video.h b/system/Video.h new file mode 100644 index 0000000..eb5bbdd --- /dev/null +++ b/system/Video.h @@ -0,0 +1,50 @@ +#ifndef VIDEO_H +#define VIDEO_H + +#include + +#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]{}; + uint8_t *_fb{nullptr}; + 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/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