Merge branch 'master' of https://git.do.srb2.org/KartKrew/Kart into acs

# Conflicts:
#	src/CMakeLists.txt
This commit is contained in:
toaster 2023-01-21 15:32:13 +00:00
commit d2d7421072
143 changed files with 23128 additions and 9460 deletions

View file

@ -6,6 +6,8 @@ UseTab: Always
TabWidth: 4
ColumnLimit: 120
AccessModifierOffset: -4
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
@ -13,10 +15,11 @@ AllowShortFunctionsOnASingleLine: InlineOnly
AllowShortIfStatementsOnASingleLine: false
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlignAfterOpenBracket: BlockIndent
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Attach # K&R/OTBS, braces on same line, Java style
BreakBeforeBraces: Allman # Always break before braces, to match existing SRB2 code
BreakConstructorInitializers: BeforeComma
CompactNamespaces: true
ConstructorInitializerAllOnOneLineOrOnePerLine: true

View file

@ -26,6 +26,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
font.c
hu_stuff.c
i_time.c
i_video_common.cpp
y_inter.c
st_stuff.c
m_aatree.c
@ -120,7 +121,6 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
k_grandprix.c
k_boss.c
k_hud.c
k_menudef.c
k_menufunc.c
k_menudraw.c
k_terrain.c
@ -225,6 +225,9 @@ target_sources(SRB2SDL2 PRIVATE discord.c stun.c)
target_link_libraries(SRB2SDL2 PRIVATE tcbrindle::span)
target_link_libraries(SRB2SDL2 PRIVATE stb_vorbis)
target_link_libraries(SRB2SDL2 PRIVATE xmp-lite::xmp-lite)
target_link_libraries(SRB2SDL2 PRIVATE glad::glad)
target_link_libraries(SRB2SDL2 PRIVATE fmt)
target_link_libraries(SRB2SDL2 PRIVATE imgui::imgui)
target_link_libraries(SRB2SDL2 PRIVATE acsvm::acsvm)
@ -531,11 +534,15 @@ if(SRB2_CONFIG_PROFILEMODE AND "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
endif()
add_subdirectory(audio)
add_subdirectory(core)
add_subdirectory(hwr2)
add_subdirectory(io)
add_subdirectory(sdl)
add_subdirectory(objects)
add_subdirectory(acs)
add_subdirectory(rhi)
add_subdirectory(tests)
add_subdirectory(menus)
# strip debug symbols into separate file when using gcc.
# to be consistent with Makefile, don't generate for OS X.

View file

@ -30,20 +30,26 @@ using std::size_t;
using namespace srb2::audio;
using namespace srb2;
namespace {
namespace
{
// Utility for leveraging Resampler...
class SoundChunkSource : public Source<1> {
class SoundChunkSource : public Source<1>
{
public:
explicit SoundChunkSource(std::unique_ptr<SoundChunk>&& chunk)
: chunk_(std::forward<std::unique_ptr<SoundChunk>>(chunk)) {}
: chunk_(std::forward<std::unique_ptr<SoundChunk>>(chunk))
{
}
virtual size_t generate(tcb::span<Sample<1>> buffer) override final {
virtual size_t generate(tcb::span<Sample<1>> buffer) override final
{
if (!chunk_)
return 0;
size_t written = 0;
for (; pos_ < chunk_->samples.size() && written < buffer.size(); pos_++) {
for (; pos_ < chunk_->samples.size() && written < buffer.size(); pos_++)
{
buffer[written] = chunk_->samples[pos_];
written++;
}
@ -56,13 +62,15 @@ private:
};
template <class I>
std::vector<Sample<1>> generate_to_vec(I& source, std::size_t estimate = 0) {
std::vector<Sample<1>> generate_to_vec(I& source, std::size_t estimate = 0)
{
std::vector<Sample<1>> generated;
size_t total = 0;
size_t read = 0;
generated.reserve(estimate);
do {
do
{
generated.resize(total + 4096);
read = source.generate(tcb::span {generated.data() + total, 4096});
total += read;
@ -71,7 +79,8 @@ std::vector<Sample<1>> generate_to_vec(I& source, std::size_t estimate = 0) {
return generated;
}
optional<SoundChunk> try_load_dmx(tcb::span<std::byte> data) {
optional<SoundChunk> try_load_dmx(tcb::span<std::byte> data)
{
io::SpanStream stream {data};
if (io::remaining(stream) < 8)
@ -90,14 +99,16 @@ optional<SoundChunk> try_load_dmx(tcb::span<std::byte> data) {
stream.seek(io::SeekFrom::kCurrent, 16);
std::vector<Sample<1>> samples;
for (size_t i = 0; i < length; i++) {
for (size_t i = 0; i < length; i++)
{
uint8_t doom_sample = io::read_uint8(stream);
float float_sample = audio::sample_to_float(doom_sample);
samples.push_back(Sample<1> {float_sample});
}
size_t samples_len = samples.size();
if (rate == 44100) {
if (rate == 44100)
{
return SoundChunk {samples};
}
@ -110,7 +121,8 @@ optional<SoundChunk> try_load_dmx(tcb::span<std::byte> data) {
size_t total = 0;
size_t read = 0;
resampled.reserve(samples_len * (static_cast<float>(kSampleRate) / rate));
do {
do
{
resampled.resize(total + 4096);
read = resampler.generate(tcb::span {resampled.data() + total, 4096});
total += read;
@ -120,34 +132,44 @@ optional<SoundChunk> try_load_dmx(tcb::span<std::byte> data) {
return SoundChunk {std::move(resampled)};
}
optional<SoundChunk> try_load_wav(tcb::span<std::byte> data) {
optional<SoundChunk> try_load_wav(tcb::span<std::byte> data)
{
io::SpanStream stream {data};
audio::Wav wav;
std::size_t sample_rate;
try {
try
{
wav = audio::load_wav(stream);
} catch (const std::exception& ex) {
}
catch (const std::exception& ex)
{
return nullopt;
}
sample_rate = wav.sample_rate();
audio::Resampler<1> resampler(std::make_unique<WavPlayer>(std::move(wav)),
sample_rate / static_cast<float>(kSampleRate));
audio::Resampler<1> resampler(
std::make_unique<WavPlayer>(std::move(wav)),
sample_rate / static_cast<float>(kSampleRate)
);
SoundChunk chunk {generate_to_vec(resampler)};
return chunk;
}
optional<SoundChunk> try_load_ogg(tcb::span<std::byte> data) {
optional<SoundChunk> try_load_ogg(tcb::span<std::byte> data)
{
std::shared_ptr<audio::OggPlayer<1>> player;
try {
try
{
io::SpanStream data_stream {data};
audio::Ogg ogg = audio::load_ogg(data_stream);
player = std::make_shared<audio::OggPlayer<1>>(std::move(ogg));
} catch (...) {
}
catch (...)
{
return nullopt;
}
player->looping(false);
@ -161,19 +183,26 @@ optional<SoundChunk> try_load_ogg(tcb::span<std::byte> data) {
return chunk;
}
optional<SoundChunk> try_load_gme(tcb::span<std::byte> data) {
optional<SoundChunk> try_load_gme(tcb::span<std::byte> data)
{
std::shared_ptr<audio::GmePlayer<1>> player;
try {
if (data[0] == std::byte {0x1F} && data[1] == std::byte {0x8B}) {
try
{
if (data[0] == std::byte {0x1F} && data[1] == std::byte {0x8B})
{
io::SpanStream stream {data};
audio::Gme gme = audio::load_gme(stream);
player = std::make_shared<GmePlayer<1>>(std::move(gme));
} else {
}
else
{
io::ZlibInputStream stream {io::SpanStream(data)};
audio::Gme gme = audio::load_gme(stream);
player = std::make_shared<GmePlayer<1>>(std::move(gme));
}
} catch (...) {
}
catch (...)
{
return nullopt;
}
std::vector<Sample<1>> samples {generate_to_vec(*player)};
@ -183,7 +212,8 @@ optional<SoundChunk> try_load_gme(tcb::span<std::byte> data) {
} // namespace
optional<SoundChunk> srb2::audio::try_load_chunk(tcb::span<std::byte> data) {
optional<SoundChunk> srb2::audio::try_load_chunk(tcb::span<std::byte> data)
{
optional<SoundChunk> ret;
ret = try_load_dmx(data);

View file

@ -17,7 +17,8 @@
#include "sound_chunk.hpp"
namespace srb2::audio {
namespace srb2::audio
{
/// @brief Try to load a chunk from the given byte span.
std::optional<SoundChunk> try_load_chunk(tcb::span<std::byte> data);

View file

@ -17,8 +17,10 @@ using namespace srb2::audio;
ExpandMono::~ExpandMono() = default;
size_t ExpandMono::filter(tcb::span<Sample<1>> input_buffer, tcb::span<Sample<2>> buffer) {
for (size_t i = 0; i < std::min(input_buffer.size(), buffer.size()); i++) {
size_t ExpandMono::filter(tcb::span<Sample<1>> input_buffer, tcb::span<Sample<2>> buffer)
{
for (size_t i = 0; i < std::min(input_buffer.size(), buffer.size()); i++)
{
buffer[i].amplitudes[0] = input_buffer[i].amplitudes[0];
buffer[i].amplitudes[1] = input_buffer[i].amplitudes[0];
}

View file

@ -14,9 +14,11 @@
#include "filter.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class ExpandMono : public Filter<1, 2> {
class ExpandMono : public Filter<1, 2>
{
public:
virtual ~ExpandMono();
virtual std::size_t filter(tcb::span<Sample<1>> input_buffer, tcb::span<Sample<2>> buffer) override final;

View file

@ -17,7 +17,8 @@ using srb2::audio::Sample;
using srb2::audio::Source;
template <size_t IC, size_t OC>
size_t Filter<IC, OC>::generate(tcb::span<Sample<OC>> buffer) {
size_t Filter<IC, OC>::generate(tcb::span<Sample<OC>> buffer)
{
input_buffer_.clear();
input_buffer_.resize(buffer.size());
@ -27,7 +28,8 @@ size_t Filter<IC, OC>::generate(tcb::span<Sample<OC>> buffer) {
}
template <size_t IC, size_t OC>
void Filter<IC, OC>::bind(const shared_ptr<Source<IC>>& input) {
void Filter<IC, OC>::bind(const shared_ptr<Source<IC>>& input)
{
input_ = input;
}

View file

@ -18,10 +18,12 @@
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t IC, size_t OC>
class Filter : public Source<OC> {
class Filter : public Source<OC>
{
public:
virtual std::size_t generate(tcb::span<Sample<OC>> buffer) override;

View file

@ -20,9 +20,11 @@ using srb2::audio::Sample;
constexpr const float kGainInterpolationAlpha = 0.8f;
template <size_t C>
size_t Gain<C>::filter(tcb::span<Sample<C>> input_buffer, tcb::span<Sample<C>> buffer) {
size_t Gain<C>::filter(tcb::span<Sample<C>> input_buffer, tcb::span<Sample<C>> buffer)
{
size_t written = std::min(buffer.size(), input_buffer.size());
for (size_t i = 0; i < written; i++) {
for (size_t i = 0; i < written; i++)
{
buffer[i] = input_buffer[i];
buffer[i] *= gain_;
gain_ += (new_gain_ - gain_) * kGainInterpolationAlpha;
@ -32,7 +34,8 @@ size_t Gain<C>::filter(tcb::span<Sample<C>> input_buffer, tcb::span<Sample<C>> b
}
template <size_t C>
void Gain<C>::gain(float new_gain) {
void Gain<C>::gain(float new_gain)
{
new_gain_ = std::clamp(new_gain, 0.0f, 1.0f);
}

View file

@ -14,10 +14,12 @@
#include "filter.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class Gain : public Filter<C, C> {
class Gain : public Filter<C, C>
{
public:
virtual std::size_t filter(tcb::span<Sample<C>> input_buffer, tcb::span<Sample<C>> buffer) override final;
void gain(float new_gain);

View file

@ -17,37 +17,45 @@
using namespace srb2;
using namespace srb2::audio;
Gme::Gme() : memory_data_(), instance_(nullptr) {
Gme::Gme() : memory_data_(), instance_(nullptr)
{
}
Gme::Gme(Gme&& rhs) noexcept : memory_data_(), instance_(nullptr) {
Gme::Gme(Gme&& rhs) noexcept : memory_data_(), instance_(nullptr)
{
std::swap(memory_data_, rhs.memory_data_);
std::swap(instance_, rhs.instance_);
}
Gme::Gme(std::vector<std::byte>&& data) : memory_data_(std::move(data)), instance_(nullptr) {
Gme::Gme(std::vector<std::byte>&& data) : memory_data_(std::move(data)), instance_(nullptr)
{
_init_with_data();
}
Gme::Gme(tcb::span<std::byte> data) : memory_data_(data.begin(), data.end()), instance_(nullptr) {
Gme::Gme(tcb::span<std::byte> data) : memory_data_(data.begin(), data.end()), instance_(nullptr)
{
_init_with_data();
}
Gme& Gme::operator=(Gme&& rhs) noexcept {
Gme& Gme::operator=(Gme&& rhs) noexcept
{
std::swap(memory_data_, rhs.memory_data_);
std::swap(instance_, rhs.instance_);
return *this;
}
Gme::~Gme() {
if (instance_) {
Gme::~Gme()
{
if (instance_)
{
gme_delete(instance_);
instance_ = nullptr;
}
}
std::size_t Gme::get_samples(tcb::span<short> buffer) {
std::size_t Gme::get_samples(tcb::span<short> buffer)
{
SRB2_ASSERT(instance_ != nullptr);
gme_err_t err = gme_play(instance_, buffer.size(), buffer.data());
@ -57,13 +65,15 @@ std::size_t Gme::get_samples(tcb::span<short> buffer) {
return buffer.size();
}
void Gme::seek(int sample) {
void Gme::seek(int sample)
{
SRB2_ASSERT(instance_ != nullptr);
gme_seek_samples(instance_, sample);
}
std::optional<float> Gme::duration_seconds() const {
std::optional<float> Gme::duration_seconds() const
{
SRB2_ASSERT(instance_ != nullptr);
gme_info_t* info = nullptr;
@ -79,7 +89,8 @@ std::optional<float> Gme::duration_seconds() const {
return static_cast<float>(info->length) / 1000.f;
}
std::optional<float> Gme::loop_point_seconds() const {
std::optional<float> Gme::loop_point_seconds() const
{
SRB2_ASSERT(instance_ != nullptr);
gme_info_t* info = nullptr;
@ -95,7 +106,8 @@ std::optional<float> Gme::loop_point_seconds() const {
return loop_point_ms / 44100.f;
}
float Gme::position_seconds() const {
float Gme::position_seconds() const
{
SRB2_ASSERT(instance_ != nullptr);
gme_info_t* info = nullptr;
@ -117,8 +129,10 @@ float Gme::position_seconds() const {
return position / 1000.f;
}
void Gme::_init_with_data() {
if (instance_) {
void Gme::_init_with_data()
{
if (instance_)
{
return;
}

View file

@ -24,9 +24,11 @@
#include "../io/streams.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class GmeException : public std::exception {
class GmeException : public std::exception
{
std::string msg_;
public:
@ -35,7 +37,8 @@ public:
virtual const char* what() const noexcept override { return msg_.c_str(); }
};
class Gme {
class Gme
{
std::vector<std::byte> memory_data_;
Music_Emu* instance_;
@ -64,7 +67,8 @@ private:
};
template <typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Gme load_gme(I& stream) {
inline Gme load_gme(I& stream)
{
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Gme {std::move(data)};
}

View file

@ -13,7 +13,8 @@ using namespace srb2;
using namespace srb2::audio;
template <size_t C>
GmePlayer<C>::GmePlayer(Gme&& gme) : gme_(std::forward<Gme>(gme)), buf_() {
GmePlayer<C>::GmePlayer(Gme&& gme) : gme_(std::forward<Gme>(gme)), buf_()
{
}
template <size_t C>
@ -26,17 +27,22 @@ template <size_t C>
GmePlayer<C>::~GmePlayer() = default;
template <size_t C>
std::size_t GmePlayer<C>::generate(tcb::span<Sample<C>> buffer) {
std::size_t GmePlayer<C>::generate(tcb::span<Sample<C>> buffer)
{
buf_.clear();
buf_.resize(buffer.size() * 2);
std::size_t read = gme_.get_samples(tcb::make_span(buf_));
buf_.resize(read);
std::size_t new_samples = std::min((read / 2), buffer.size());
for (std::size_t i = 0; i < new_samples; i++) {
if constexpr (C == 1) {
for (std::size_t i = 0; i < new_samples; i++)
{
if constexpr (C == 1)
{
buffer[i].amplitudes[0] = (buf_[i * 2] / 32768.f + buf_[i * 2 + 1] / 32768.f) / 2.f;
} else if constexpr (C == 2) {
}
else if constexpr (C == 2)
{
buffer[i].amplitudes[0] = buf_[i * 2] / 32768.f;
buffer[i].amplitudes[1] = buf_[i * 2 + 1] / 32768.f;
}
@ -45,27 +51,32 @@ std::size_t GmePlayer<C>::generate(tcb::span<Sample<C>> buffer) {
}
template <size_t C>
void GmePlayer<C>::seek(float position_seconds) {
void GmePlayer<C>::seek(float position_seconds)
{
gme_.seek(static_cast<std::size_t>(position_seconds * 44100.f));
}
template <size_t C>
void GmePlayer<C>::reset() {
void GmePlayer<C>::reset()
{
gme_.seek(0);
}
template <size_t C>
std::optional<float> GmePlayer<C>::duration_seconds() const {
std::optional<float> GmePlayer<C>::duration_seconds() const
{
return gme_.duration_seconds();
}
template <size_t C>
std::optional<float> GmePlayer<C>::loop_point_seconds() const {
std::optional<float> GmePlayer<C>::loop_point_seconds() const
{
return gme_.loop_point_seconds();
}
template <size_t C>
float GmePlayer<C>::position_seconds() const {
float GmePlayer<C>::position_seconds() const
{
return gme_.position_seconds();
}

View file

@ -15,10 +15,12 @@
#include "gme.hpp"
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class GmePlayer : public Source<C> {
class GmePlayer : public Source<C>
{
Gme gme_;
std::vector<short> buf_;

View file

@ -18,16 +18,20 @@ using srb2::audio::Mixer;
using srb2::audio::Sample;
using srb2::audio::Source;
namespace {
namespace
{
template <size_t C>
void default_init_sample_buffer(Sample<C>* buffer, size_t size) {
void default_init_sample_buffer(Sample<C>* buffer, size_t size)
{
std::for_each(buffer, buffer + size, [](auto& i) { i = Sample<C> {}; });
}
template <size_t C>
void mix_sample_buffers(Sample<C>* dst, size_t size, Sample<C>* src, size_t src_size) {
for (size_t i = 0; i < size && i < src_size; i++) {
void mix_sample_buffers(Sample<C>* dst, size_t size, Sample<C>* src, size_t src_size)
{
for (size_t i = 0; i < size && i < src_size; i++)
{
dst[i] += src[i];
}
}
@ -35,12 +39,14 @@ void mix_sample_buffers(Sample<C>* dst, size_t size, Sample<C>* src, size_t src_
} // namespace
template <size_t C>
size_t Mixer<C>::generate(tcb::span<Sample<C>> buffer) {
size_t Mixer<C>::generate(tcb::span<Sample<C>> buffer)
{
buffer_.resize(buffer.size());
default_init_sample_buffer<C>(buffer.data(), buffer.size());
for (auto& source : sources_) {
for (auto& source : sources_)
{
size_t read = source->generate(buffer_);
mix_sample_buffers<C>(buffer.data(), buffer.size(), buffer_.data(), read);
@ -51,7 +57,8 @@ size_t Mixer<C>::generate(tcb::span<Sample<C>> buffer) {
}
template <size_t C>
void Mixer<C>::add_source(const shared_ptr<Source<C>>& source) {
void Mixer<C>::add_source(const shared_ptr<Source<C>>& source)
{
sources_.push_back(source);
}

View file

@ -17,10 +17,12 @@
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class Mixer : public Source<C> {
class Mixer : public Source<C>
{
public:
virtual std::size_t generate(tcb::span<Sample<C>> buffer) override final;

View file

@ -43,12 +43,14 @@ using srb2::audio::Sample;
using srb2::audio::Source;
using namespace srb2;
class MusicPlayer::Impl {
class MusicPlayer::Impl
{
public:
Impl() = default;
Impl(tcb::span<std::byte> data) : Impl() { _load(data); }
size_t generate(tcb::span<Sample<2>> buffer) {
size_t generate(tcb::span<Sample<2>> buffer)
{
if (!resampler_)
return 0;
@ -57,12 +59,14 @@ public:
size_t total_written = 0;
while (total_written < buffer.size()) {
while (total_written < buffer.size())
{
const size_t generated = resampler_->generate(buffer.subspan(total_written));
// To avoid a branch preventing optimizations, we're always going to apply
// the fade gain, even if it would clamp anyway.
for (std::size_t i = 0; i < generated; i++) {
for (std::size_t i = 0; i < generated; i++)
{
const float alpha = 1.0 - (gain_samples_target_ - std::min(gain_samples_ + i, gain_samples_target_)) /
static_cast<double>(gain_samples_target_);
const float fade_gain = (gain_target_ - gain_) * std::clamp(alpha, 0.f, 1.f) + gain_;
@ -71,7 +75,8 @@ public:
gain_samples_ = std::min(gain_samples_ + generated, gain_samples_target_);
if (gain_samples_ >= gain_samples_target_) {
if (gain_samples_ >= gain_samples_target_)
{
fading_ = false;
gain_samples_ = gain_samples_target_;
gain_ = gain_target_;
@ -79,7 +84,8 @@ public:
total_written += generated;
if (generated == 0) {
if (generated == 0)
{
playing_ = false;
break;
}
@ -88,53 +94,68 @@ public:
return total_written;
}
void _load(tcb::span<std::byte> data) {
void _load(tcb::span<std::byte> data)
{
ogg_inst_ = nullptr;
gme_inst_ = nullptr;
xmp_inst_ = nullptr;
resampler_ = std::nullopt;
try {
try
{
io::SpanStream stream {data};
audio::Ogg ogg = audio::load_ogg(stream);
ogg_inst_ = std::make_shared<audio::OggPlayer<2>>(std::move(ogg));
ogg_inst_->looping(looping_);
resampler_ = Resampler<2>(ogg_inst_, ogg_inst_->sample_rate() / 44100.f);
} catch (const std::exception& ex) {
}
catch (const std::exception& ex)
{
// it's probably not ogg
ogg_inst_ = nullptr;
resampler_ = std::nullopt;
}
if (!resampler_) {
try {
if (data[0] == std::byte {0x1F} && data[1] == std::byte {0x8B}) {
if (!resampler_)
{
try
{
if (data[0] == std::byte {0x1F} && data[1] == std::byte {0x8B})
{
io::ZlibInputStream stream {io::SpanStream(data)};
audio::Gme gme = audio::load_gme(stream);
gme_inst_ = std::make_shared<GmePlayer<2>>(std::move(gme));
} else {
}
else
{
io::SpanStream stream {data};
audio::Gme gme = audio::load_gme(stream);
gme_inst_ = std::make_shared<GmePlayer<2>>(std::move(gme));
}
resampler_ = Resampler<2>(gme_inst_, 1.f);
} catch (const std::exception& ex) {
}
catch (const std::exception& ex)
{
// it's probably not gme
gme_inst_ = nullptr;
resampler_ = std::nullopt;
}
}
if (!resampler_) {
try {
if (!resampler_)
{
try
{
io::SpanStream stream {data};
audio::Xmp<2> xmp = audio::load_xmp<2>(stream);
xmp_inst_ = std::make_shared<XmpPlayer<2>>(std::move(xmp));
xmp_inst_->looping(looping_);
resampler_ = Resampler<2>(xmp_inst_, 1.f);
} catch (const std::exception& ex) {
}
catch (const std::exception& ex)
{
// it's probably not xmp
xmp_inst_ = nullptr;
resampler_ = std::nullopt;
@ -146,74 +167,103 @@ public:
internal_gain(1.f);
}
void play(bool looping) {
if (ogg_inst_) {
void play(bool looping)
{
if (ogg_inst_)
{
ogg_inst_->looping(looping);
ogg_inst_->playing(true);
playing_ = true;
ogg_inst_->reset();
} else if (gme_inst_) {
}
else if (gme_inst_)
{
playing_ = true;
gme_inst_->reset();
} else if (xmp_inst_) {
}
else if (xmp_inst_)
{
xmp_inst_->looping(looping);
playing_ = true;
xmp_inst_->reset();
}
}
void unpause() {
if (ogg_inst_) {
void unpause()
{
if (ogg_inst_)
{
ogg_inst_->playing(true);
playing_ = true;
} else if (gme_inst_) {
}
else if (gme_inst_)
{
playing_ = true;
} else if (xmp_inst_) {
}
else if (xmp_inst_)
{
playing_ = true;
}
}
void pause() {
if (ogg_inst_) {
void pause()
{
if (ogg_inst_)
{
ogg_inst_->playing(false);
playing_ = false;
} else if (gme_inst_) {
}
else if (gme_inst_)
{
playing_ = false;
} else if (xmp_inst_) {
}
else if (xmp_inst_)
{
playing_ = false;
}
}
void stop() {
if (ogg_inst_) {
void stop()
{
if (ogg_inst_)
{
ogg_inst_->reset();
ogg_inst_->playing(false);
playing_ = false;
} else if (gme_inst_) {
}
else if (gme_inst_)
{
gme_inst_->reset();
playing_ = false;
} else if (xmp_inst_) {
}
else if (xmp_inst_)
{
xmp_inst_->reset();
playing_ = false;
}
}
void seek(float position_seconds) {
if (ogg_inst_) {
void seek(float position_seconds)
{
if (ogg_inst_)
{
ogg_inst_->seek(position_seconds);
return;
}
if (gme_inst_) {
if (gme_inst_)
{
gme_inst_->seek(position_seconds);
return;
}
if (xmp_inst_) {
if (xmp_inst_)
{
xmp_inst_->seek(position_seconds);
return;
}
}
bool playing() const {
bool playing() const
{
if (ogg_inst_)
return ogg_inst_->playing();
else if (gme_inst_)
@ -224,7 +274,8 @@ public:
return false;
}
std::optional<audio::MusicType> music_type() const {
std::optional<audio::MusicType> music_type() const
{
if (ogg_inst_)
return audio::MusicType::kOgg;
else if (gme_inst_)
@ -235,7 +286,8 @@ public:
return std::nullopt;
}
std::optional<float> duration_seconds() const {
std::optional<float> duration_seconds() const
{
if (ogg_inst_)
return ogg_inst_->duration_seconds();
if (gme_inst_)
@ -246,7 +298,8 @@ public:
return std::nullopt;
}
std::optional<float> loop_point_seconds() const {
std::optional<float> loop_point_seconds() const
{
if (ogg_inst_)
return ogg_inst_->loop_point_seconds();
if (gme_inst_)
@ -255,7 +308,8 @@ public:
return std::nullopt;
}
std::optional<float> position_seconds() const {
std::optional<float> position_seconds() const
{
if (ogg_inst_)
return ogg_inst_->position_seconds();
if (gme_inst_)
@ -266,13 +320,14 @@ public:
void fade_to(float gain, float seconds) { fade_from_to(gain_target_, gain, seconds); }
void fade_from_to(float from, float to, float seconds) {
void fade_from_to(float from, float to, float seconds)
{
fading_ = true;
gain_ = from;
gain_target_ = to;
// Gain samples target must always be at least 1 to avoid a div-by-zero.
gain_samples_target_ = std::max(
static_cast<uint64_t>(seconds * 44100.f), UINT64_C(1)); // UINT64_C generates a uint64_t literal
gain_samples_target_ =
std::max(static_cast<uint64_t>(seconds * 44100.f), UINT64_C(1)); // UINT64_C generates a uint64_t literal
gain_samples_ = 0;
}
@ -280,12 +335,14 @@ public:
void stop_fade() { internal_gain(gain_target_); }
void loop_point_seconds(float loop_point) {
void loop_point_seconds(float loop_point)
{
if (ogg_inst_)
ogg_inst_->loop_point_seconds(loop_point);
}
void internal_gain(float gain) {
void internal_gain(float gain)
{
fading_ = false;
gain_ = gain;
gain_target_ = gain;
@ -310,112 +367,131 @@ private:
};
// The special member functions MUST be declared in this unit, where Impl is complete.
MusicPlayer::MusicPlayer() : impl_(make_unique<MusicPlayer::Impl>()) {
MusicPlayer::MusicPlayer() : impl_(make_unique<MusicPlayer::Impl>())
{
}
MusicPlayer::MusicPlayer(tcb::span<std::byte> data) : impl_(make_unique<MusicPlayer::Impl>(data)) {
MusicPlayer::MusicPlayer(tcb::span<std::byte> data) : impl_(make_unique<MusicPlayer::Impl>(data))
{
}
MusicPlayer::MusicPlayer(MusicPlayer&& rhs) noexcept = default;
MusicPlayer& MusicPlayer::operator=(MusicPlayer&& rhs) noexcept = default;
MusicPlayer::~MusicPlayer() = default;
void MusicPlayer::play(bool looping) {
void MusicPlayer::play(bool looping)
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->play(looping);
}
void MusicPlayer::unpause() {
void MusicPlayer::unpause()
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->unpause();
}
void MusicPlayer::pause() {
void MusicPlayer::pause()
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->pause();
}
void MusicPlayer::stop() {
void MusicPlayer::stop()
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->stop();
}
void MusicPlayer::seek(float position_seconds) {
void MusicPlayer::seek(float position_seconds)
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->seek(position_seconds);
}
bool MusicPlayer::playing() const {
bool MusicPlayer::playing() const
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->playing();
}
size_t MusicPlayer::generate(tcb::span<Sample<2>> buffer) {
size_t MusicPlayer::generate(tcb::span<Sample<2>> buffer)
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->generate(buffer);
}
std::optional<audio::MusicType> MusicPlayer::music_type() const {
std::optional<audio::MusicType> MusicPlayer::music_type() const
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->music_type();
}
std::optional<float> MusicPlayer::duration_seconds() const {
std::optional<float> MusicPlayer::duration_seconds() const
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->duration_seconds();
}
std::optional<float> MusicPlayer::loop_point_seconds() const {
std::optional<float> MusicPlayer::loop_point_seconds() const
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->loop_point_seconds();
}
std::optional<float> MusicPlayer::position_seconds() const {
std::optional<float> MusicPlayer::position_seconds() const
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->position_seconds();
}
void MusicPlayer::fade_to(float gain, float seconds) {
void MusicPlayer::fade_to(float gain, float seconds)
{
SRB2_ASSERT(impl_ != nullptr);
impl_->fade_to(gain, seconds);
}
void MusicPlayer::fade_from_to(float from, float to, float seconds) {
void MusicPlayer::fade_from_to(float from, float to, float seconds)
{
SRB2_ASSERT(impl_ != nullptr);
impl_->fade_from_to(from, to, seconds);
}
void MusicPlayer::internal_gain(float gain) {
void MusicPlayer::internal_gain(float gain)
{
SRB2_ASSERT(impl_ != nullptr);
impl_->internal_gain(gain);
}
bool MusicPlayer::fading() const {
bool MusicPlayer::fading() const
{
SRB2_ASSERT(impl_ != nullptr);
return impl_->fading();
}
void MusicPlayer::stop_fade() {
void MusicPlayer::stop_fade()
{
SRB2_ASSERT(impl_ != nullptr);
impl_->stop_fade();
}
void MusicPlayer::loop_point_seconds(float loop_point) {
void MusicPlayer::loop_point_seconds(float loop_point)
{
SRB2_ASSERT(impl_ != nullptr);
impl_->loop_point_seconds(loop_point);

View file

@ -19,15 +19,18 @@
struct stb_vorbis;
namespace srb2::audio {
namespace srb2::audio
{
enum class MusicType {
enum class MusicType
{
kOgg,
kGme,
kMod
};
class MusicPlayer : public Source<2> {
class MusicPlayer : public Source<2>
{
public:
MusicPlayer();
MusicPlayer(tcb::span<std::byte> data);

View file

@ -16,11 +16,14 @@
using namespace srb2;
using namespace srb2::audio;
StbVorbisException::StbVorbisException(int code) noexcept : code_(code) {
StbVorbisException::StbVorbisException(int code) noexcept : code_(code)
{
}
const char* StbVorbisException::what() const noexcept {
switch (code_) {
const char* StbVorbisException::what() const noexcept
{
switch (code_)
{
case VORBIS__no_error:
return "No error";
case VORBIS_need_more_data:
@ -68,54 +71,73 @@ const char* StbVorbisException::what() const noexcept {
}
}
Ogg::Ogg() noexcept : memory_data_(), instance_(nullptr) {
Ogg::Ogg() noexcept : memory_data_(), instance_(nullptr)
{
}
Ogg::Ogg(std::vector<std::byte> data) : memory_data_(std::move(data)), instance_(nullptr) {
Ogg::Ogg(std::vector<std::byte> data) : memory_data_(std::move(data)), instance_(nullptr)
{
_init_with_data();
}
Ogg::Ogg(tcb::span<std::byte> data) : memory_data_(data.begin(), data.end()), instance_(nullptr) {
Ogg::Ogg(tcb::span<std::byte> data) : memory_data_(data.begin(), data.end()), instance_(nullptr)
{
_init_with_data();
}
Ogg::Ogg(Ogg&& rhs) noexcept : memory_data_(), instance_(nullptr) {
Ogg::Ogg(Ogg&& rhs) noexcept : memory_data_(), instance_(nullptr)
{
std::swap(memory_data_, rhs.memory_data_);
std::swap(instance_, rhs.instance_);
}
Ogg& Ogg::operator=(Ogg&& rhs) noexcept {
Ogg& Ogg::operator=(Ogg&& rhs) noexcept
{
std::swap(memory_data_, rhs.memory_data_);
std::swap(instance_, rhs.instance_);
return *this;
}
Ogg::~Ogg() {
if (instance_) {
Ogg::~Ogg()
{
if (instance_)
{
stb_vorbis_close(instance_);
instance_ = nullptr;
}
}
std::size_t Ogg::get_samples(tcb::span<Sample<1>> buffer) {
std::size_t Ogg::get_samples(tcb::span<Sample<1>> buffer)
{
SRB2_ASSERT(instance_ != nullptr);
size_t read = stb_vorbis_get_samples_float_interleaved(
instance_, 1, reinterpret_cast<float*>(buffer.data()), buffer.size() * 1);
instance_,
1,
reinterpret_cast<float*>(buffer.data()),
buffer.size() * 1
);
return read;
}
std::size_t Ogg::get_samples(tcb::span<Sample<2>> buffer) {
std::size_t Ogg::get_samples(tcb::span<Sample<2>> buffer)
{
SRB2_ASSERT(instance_ != nullptr);
size_t read = stb_vorbis_get_samples_float_interleaved(
instance_, 2, reinterpret_cast<float*>(buffer.data()), buffer.size() * 2);
instance_,
2,
reinterpret_cast<float*>(buffer.data()),
buffer.size() * 2
);
stb_vorbis_info info = stb_vorbis_get_info(instance_);
if (info.channels == 1) {
for (auto& sample : buffer.subspan(0, read)) {
if (info.channels == 1)
{
for (auto& sample : buffer.subspan(0, read))
{
sample.amplitudes[1] = sample.amplitudes[0];
}
}
@ -123,7 +145,8 @@ std::size_t Ogg::get_samples(tcb::span<Sample<2>> buffer) {
return read;
}
OggComment Ogg::comment() const {
OggComment Ogg::comment() const
{
SRB2_ASSERT(instance_ != nullptr);
stb_vorbis_comment c_comment = stb_vorbis_get_comment(instance_);
@ -133,50 +156,59 @@ OggComment Ogg::comment() const {
std::vector<std::string>(c_comment.comment_list, c_comment.comment_list + c_comment.comment_list_length)};
}
std::size_t Ogg::sample_rate() const {
std::size_t Ogg::sample_rate() const
{
SRB2_ASSERT(instance_ != nullptr);
stb_vorbis_info info = stb_vorbis_get_info(instance_);
return info.sample_rate;
}
void Ogg::seek(std::size_t sample) {
void Ogg::seek(std::size_t sample)
{
SRB2_ASSERT(instance_ != nullptr);
stb_vorbis_seek(instance_, sample);
}
std::size_t Ogg::position() const {
std::size_t Ogg::position() const
{
SRB2_ASSERT(instance_ != nullptr);
return stb_vorbis_get_sample_offset(instance_);
}
float Ogg::position_seconds() const {
float Ogg::position_seconds() const
{
return position() / static_cast<float>(sample_rate());
}
std::size_t Ogg::duration_samples() const {
std::size_t Ogg::duration_samples() const
{
SRB2_ASSERT(instance_ != nullptr);
return stb_vorbis_stream_length_in_samples(instance_);
}
float Ogg::duration_seconds() const {
float Ogg::duration_seconds() const
{
SRB2_ASSERT(instance_ != nullptr);
return stb_vorbis_stream_length_in_seconds(instance_);
}
std::size_t Ogg::channels() const {
std::size_t Ogg::channels() const
{
SRB2_ASSERT(instance_ != nullptr);
stb_vorbis_info info = stb_vorbis_get_info(instance_);
return info.channels;
}
void Ogg::_init_with_data() {
if (instance_) {
void Ogg::_init_with_data()
{
if (instance_)
{
return;
}
@ -187,7 +219,11 @@ void Ogg::_init_with_data() {
int vorbis_result;
instance_ = stb_vorbis_open_memory(
reinterpret_cast<const unsigned char*>(memory_data_.data()), memory_data_.size(), &vorbis_result, NULL);
reinterpret_cast<const unsigned char*>(memory_data_.data()),
memory_data_.size(),
&vorbis_result,
NULL
);
if (vorbis_result != VORBIS__no_error)
throw StbVorbisException(vorbis_result);

View file

@ -20,9 +20,11 @@
#include "../io/streams.hpp"
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class StbVorbisException final : public std::exception {
class StbVorbisException final : public std::exception
{
int code_;
public:
@ -31,12 +33,14 @@ public:
virtual const char* what() const noexcept;
};
struct OggComment {
struct OggComment
{
std::string vendor;
std::vector<std::string> comments;
};
class Ogg final {
class Ogg final
{
std::vector<std::byte> memory_data_;
stb_vorbis* instance_;
@ -71,7 +75,8 @@ private:
};
template <typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Ogg load_ogg(I& stream) {
inline Ogg load_ogg(I& stream)
{
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Ogg {std::move(data)};
}

View file

@ -18,35 +18,46 @@
using namespace srb2;
using namespace srb2::audio;
namespace {
namespace
{
std::optional<std::size_t> find_loop_point(const Ogg& ogg) {
std::optional<std::size_t> find_loop_point(const Ogg& ogg)
{
OggComment comment = ogg.comment();
std::size_t rate = ogg.sample_rate();
for (auto& comment : comment.comments) {
if (comment.find("LOOPPOINT=") == 0) {
for (auto& comment : comment.comments)
{
if (comment.find("LOOPPOINT=") == 0)
{
std::string_view comment_view(comment);
comment_view.remove_prefix(10);
std::string copied {comment_view};
try {
try
{
int loop_point = std::stoi(copied);
return loop_point;
} catch (...) {
}
catch (...)
{
}
}
if (comment.find("LOOPMS=") == 0) {
if (comment.find("LOOPMS=") == 0)
{
std::string_view comment_view(comment);
comment_view.remove_prefix(7);
std::string copied {comment_view};
try {
try
{
int loop_ms = std::stoi(copied);
int loop_point = std::round(static_cast<double>(loop_ms) / (rate / 1000.));
int loop_point = std::round(static_cast<double>(rate) * (loop_ms / 1000.));
return loop_point;
} catch (...) {
}
catch (...)
{
}
}
}
@ -58,7 +69,8 @@ std::optional<std::size_t> find_loop_point(const Ogg& ogg) {
template <size_t C>
OggPlayer<C>::OggPlayer(Ogg&& ogg) noexcept
: playing_(false), looping_(false), loop_point_(std::nullopt), ogg_(std::forward<Ogg>(ogg)) {
: playing_(false), looping_(false), loop_point_(std::nullopt), ogg_(std::forward<Ogg>(ogg))
{
loop_point_ = find_loop_point(ogg_);
}
@ -72,25 +84,30 @@ template <size_t C>
OggPlayer<C>::~OggPlayer() = default;
template <size_t C>
std::size_t OggPlayer<C>::generate(tcb::span<Sample<C>> buffer) {
std::size_t OggPlayer<C>::generate(tcb::span<Sample<C>> buffer)
{
if (!playing_)
return 0;
std::size_t total = 0;
do {
do
{
std::size_t read = ogg_.get_samples(buffer.subspan(total));
total += read;
if (read == 0 && !looping_) {
if (read == 0 && !looping_)
{
playing_ = false;
break;
}
if (read == 0 && loop_point_) {
if (read == 0 && loop_point_)
{
ogg_.seek(*loop_point_);
}
if (read == 0 && !loop_point_) {
if (read == 0 && !loop_point_)
{
ogg_.seek(0);
}
} while (total < buffer.size());
@ -99,33 +116,39 @@ std::size_t OggPlayer<C>::generate(tcb::span<Sample<C>> buffer) {
}
template <size_t C>
void OggPlayer<C>::seek(float position_seconds) {
void OggPlayer<C>::seek(float position_seconds)
{
ogg_.seek(static_cast<std::size_t>(position_seconds * sample_rate()));
}
template <size_t C>
void OggPlayer<C>::loop_point_seconds(float loop_point) {
void OggPlayer<C>::loop_point_seconds(float loop_point)
{
std::size_t rate = sample_rate();
loop_point = static_cast<std::size_t>(std::round(loop_point * rate));
}
template <size_t C>
void OggPlayer<C>::reset() {
void OggPlayer<C>::reset()
{
ogg_.seek(0);
}
template <size_t C>
std::size_t OggPlayer<C>::sample_rate() const {
std::size_t OggPlayer<C>::sample_rate() const
{
return ogg_.sample_rate();
}
template <size_t C>
float OggPlayer<C>::duration_seconds() const {
float OggPlayer<C>::duration_seconds() const
{
return ogg_.duration_seconds();
}
template <size_t C>
std::optional<float> OggPlayer<C>::loop_point_seconds() const {
std::optional<float> OggPlayer<C>::loop_point_seconds() const
{
if (!loop_point_)
return std::nullopt;
@ -133,7 +156,8 @@ std::optional<float> OggPlayer<C>::loop_point_seconds() const {
}
template <size_t C>
float OggPlayer<C>::position_seconds() const {
float OggPlayer<C>::position_seconds() const
{
return ogg_.position_seconds();
}

View file

@ -25,10 +25,12 @@
#include "ogg.hpp"
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class OggPlayer final : public Source<C> {
class OggPlayer final : public Source<C>
{
bool playing_;
bool looping_;
std::optional<std::size_t> loop_point_;

View file

@ -23,7 +23,8 @@ using namespace srb2::audio;
template <size_t C>
Resampler<C>::Resampler(std::shared_ptr<Source<C>>&& source, float ratio)
: source_(std::forward<std::shared_ptr<Source<C>>>(source)), ratio_(ratio) {
: source_(std::forward<std::shared_ptr<Source<C>>>(source)), ratio_(ratio)
{
}
template <size_t C>
@ -36,11 +37,13 @@ template <size_t C>
Resampler<C>& Resampler<C>::operator=(Resampler<C>&& r) = default;
template <size_t C>
size_t Resampler<C>::generate(tcb::span<Sample<C>> buffer) {
size_t Resampler<C>::generate(tcb::span<Sample<C>> buffer)
{
if (!source_)
return 0;
if (ratio_ == 1.f) {
if (ratio_ == 1.f)
{
// fast path - generate directly from source
size_t source_read = source_->generate(buffer);
return source_read;
@ -48,21 +51,25 @@ size_t Resampler<C>::generate(tcb::span<Sample<C>> buffer) {
size_t written = 0;
while (written < buffer.size()) {
while (written < buffer.size())
{
// do we need a refill?
if (buf_.size() == 0 || pos_ >= static_cast<int>(buf_.size() - 1)) {
if (buf_.size() == 0 || pos_ >= static_cast<int>(buf_.size() - 1))
{
pos_ -= buf_.size();
last_ = buf_.size() == 0 ? Sample<C> {} : buf_.back();
buf_.clear();
buf_.resize(512);
size_t source_read = source_->generate(buf_);
buf_.resize(source_read);
if (source_read == 0) {
if (source_read == 0)
{
break;
}
}
if (pos_ < 0) {
if (pos_ < 0)
{
buffer[written] = (buf_[0] - last_) * pos_frac_ + last_;
advance(ratio_);
written++;

View file

@ -21,10 +21,12 @@
#include "sound_chunk.hpp"
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class Resampler : public Source<C> {
class Resampler : public Source<C>
{
public:
Resampler(std::shared_ptr<Source<C>>&& source_, float ratio);
Resampler(const Resampler<C>& r) = delete;
@ -44,7 +46,8 @@ private:
int pos_ {0};
float pos_frac_ {0.f};
void advance(float samples) {
void advance(float samples)
{
pos_frac_ += samples;
float integer;
std::modf(pos_frac_, &integer);

View file

@ -12,21 +12,27 @@
#include <cstddef>
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
struct Sample {
struct Sample
{
std::array<float, C> amplitudes;
constexpr Sample& operator+=(const Sample& rhs) noexcept {
for (std::size_t i = 0; i < C; i++) {
constexpr Sample& operator+=(const Sample& rhs) noexcept
{
for (std::size_t i = 0; i < C; i++)
{
amplitudes[i] += rhs.amplitudes[i];
}
return *this;
}
constexpr Sample& operator*=(float rhs) noexcept {
for (std::size_t i = 0; i < C; i++) {
constexpr Sample& operator*=(float rhs) noexcept
{
for (std::size_t i = 0; i < C; i++)
{
amplitudes[i] *= rhs;
}
return *this;
@ -34,27 +40,33 @@ struct Sample {
};
template <size_t C>
constexpr Sample<C> operator+(const Sample<C>& lhs, const Sample<C>& rhs) noexcept {
constexpr Sample<C> operator+(const Sample<C>& lhs, const Sample<C>& rhs) noexcept
{
Sample<C> out;
for (std::size_t i = 0; i < C; i++) {
for (std::size_t i = 0; i < C; i++)
{
out.amplitudes[i] = lhs.amplitudes[i] + rhs.amplitudes[i];
}
return out;
}
template <size_t C>
constexpr Sample<C> operator-(const Sample<C>& lhs, const Sample<C>& rhs) noexcept {
constexpr Sample<C> operator-(const Sample<C>& lhs, const Sample<C>& rhs) noexcept
{
Sample<C> out;
for (std::size_t i = 0; i < C; i++) {
for (std::size_t i = 0; i < C; i++)
{
out.amplitudes[i] = lhs.amplitudes[i] - rhs.amplitudes[i];
}
return out;
}
template <size_t C>
constexpr Sample<C> operator*(const Sample<C>& lhs, float rhs) noexcept {
constexpr Sample<C> operator*(const Sample<C>& lhs, float rhs) noexcept
{
Sample<C> out;
for (std::size_t i = 0; i < C; i++) {
for (std::size_t i = 0; i < C; i++)
{
out.amplitudes[i] = lhs.amplitudes[i] * rhs;
}
return out;
@ -64,12 +76,14 @@ template <class T>
static constexpr float sample_to_float(T sample) noexcept;
template <>
constexpr float sample_to_float<uint8_t>(uint8_t sample) noexcept {
constexpr float sample_to_float<uint8_t>(uint8_t sample) noexcept
{
return (sample / 128.f) - 1.f;
}
template <>
constexpr float sample_to_float<int16_t>(int16_t sample) noexcept {
constexpr float sample_to_float<int16_t>(int16_t sample) noexcept
{
return sample / 32768.f;
}

View file

@ -14,9 +14,11 @@
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
struct SoundChunk {
struct SoundChunk
{
std::vector<Sample<1>> samples;
};

View file

@ -20,15 +20,18 @@ using srb2::audio::Sample;
using srb2::audio::SoundEffectPlayer;
using srb2::audio::Source;
size_t SoundEffectPlayer::generate(tcb::span<Sample<2>> buffer) {
size_t SoundEffectPlayer::generate(tcb::span<Sample<2>> buffer)
{
if (!chunk_)
return 0;
if (position_ >= chunk_->samples.size()) {
if (position_ >= chunk_->samples.size())
{
return 0;
}
size_t written = 0;
for (; position_ < chunk_->samples.size() && written < buffer.size(); position_++) {
for (; position_ < chunk_->samples.size() && written < buffer.size(); position_++)
{
float mono_sample = chunk_->samples[position_].amplitudes[0];
float sep_pan = ((sep_ + 1.f) / 2.f) * (3.14159 / 2.f);
@ -41,23 +44,27 @@ size_t SoundEffectPlayer::generate(tcb::span<Sample<2>> buffer) {
return written;
}
void SoundEffectPlayer::start(const SoundChunk* chunk, float volume, float sep) {
void SoundEffectPlayer::start(const SoundChunk* chunk, float volume, float sep)
{
this->update(volume, sep);
position_ = 0;
chunk_ = chunk;
}
void SoundEffectPlayer::update(float volume, float sep) {
void SoundEffectPlayer::update(float volume, float sep)
{
volume_ = volume;
sep_ = sep;
}
void SoundEffectPlayer::reset() {
void SoundEffectPlayer::reset()
{
position_ = 0;
chunk_ = nullptr;
}
bool SoundEffectPlayer::finished() const {
bool SoundEffectPlayer::finished() const
{
if (!chunk_)
return true;
if (position_ >= chunk_->samples.size())
@ -65,7 +72,8 @@ bool SoundEffectPlayer::finished() const {
return false;
}
bool SoundEffectPlayer::is_playing_chunk(const SoundChunk* chunk) const {
bool SoundEffectPlayer::is_playing_chunk(const SoundChunk* chunk) const
{
return chunk_ == chunk;
}

View file

@ -17,9 +17,11 @@
#include "sound_chunk.hpp"
#include "source.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class SoundEffectPlayer : public Source<2> {
class SoundEffectPlayer : public Source<2>
{
public:
virtual std::size_t generate(tcb::span<Sample<2>> buffer) override final;

View file

@ -16,10 +16,12 @@
#include "sample.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class Source {
class Source
{
public:
virtual std::size_t generate(tcb::span<Sample<C>> buffer) = 0;

View file

@ -13,10 +13,13 @@
#include <optional>
#include <stdexcept>
#include "../cxxutil.hpp"
using namespace srb2;
using srb2::audio::Wav;
namespace {
namespace
{
constexpr const uint32_t kMagicRIFF = 0x46464952;
constexpr const uint32_t kMagicWAVE = 0x45564157;
@ -27,17 +30,20 @@ constexpr const uint16_t kFormatPcm = 1;
constexpr const std::size_t kRiffHeaderLength = 8;
struct RiffHeader {
struct RiffHeader
{
uint32_t magic;
std::size_t filesize;
};
struct TagHeader {
struct TagHeader
{
uint32_t type;
std::size_t length;
};
struct FmtTag {
struct FmtTag
{
uint16_t format;
uint16_t channels;
uint32_t rate;
@ -46,9 +52,12 @@ struct FmtTag {
uint16_t bit_width;
};
struct DataTag {};
struct DataTag
{
};
RiffHeader parse_riff_header(io::SpanStream& stream) {
RiffHeader parse_riff_header(io::SpanStream& stream)
{
if (io::remaining(stream) < kRiffHeaderLength)
throw std::runtime_error("insufficient bytes remaining in stream");
@ -58,7 +67,8 @@ RiffHeader parse_riff_header(io::SpanStream& stream) {
return ret;
}
TagHeader parse_tag_header(io::SpanStream& stream) {
TagHeader parse_tag_header(io::SpanStream& stream)
{
if (io::remaining(stream) < 8)
throw std::runtime_error("insufficient bytes remaining in stream");
@ -68,7 +78,8 @@ TagHeader parse_tag_header(io::SpanStream& stream) {
return header;
}
FmtTag parse_fmt_tag(io::SpanStream& stream) {
FmtTag parse_fmt_tag(io::SpanStream& stream)
{
if (io::remaining(stream) < 16)
throw std::runtime_error("insufficient bytes in stream");
@ -84,14 +95,16 @@ FmtTag parse_fmt_tag(io::SpanStream& stream) {
}
template <typename Visitor>
void visit_tag(Visitor& visitor, io::SpanStream& stream, const TagHeader& header) {
void visit_tag(Visitor& visitor, io::SpanStream& stream, const TagHeader& header)
{
if (io::remaining(stream) < header.length)
throw std::runtime_error("insufficient bytes in stream");
const io::StreamSize start = stream.seek(io::SeekFrom::kCurrent, 0);
const io::StreamSize dest = start + header.length;
switch (header.type) {
switch (header.type)
{
case kMagicFmt:
{
FmtTag fmt_tag {parse_fmt_tag(stream)};
@ -112,87 +125,96 @@ void visit_tag(Visitor& visitor, io::SpanStream& stream, const TagHeader& header
stream.seek(io::SeekFrom::kStart, dest);
}
std::vector<uint8_t> read_uint8_samples_from_stream(io::SpanStream& stream, std::size_t count) {
std::vector<uint8_t> read_uint8_samples_from_stream(io::SpanStream& stream, std::size_t count)
{
std::vector<uint8_t> samples;
samples.reserve(count);
for (std::size_t i = 0; i < count; i++) {
for (std::size_t i = 0; i < count; i++)
{
samples.push_back(io::read_uint8(stream));
}
return samples;
}
std::vector<int16_t> read_int16_samples_from_stream(io::SpanStream& stream, std::size_t count) {
std::vector<int16_t> read_int16_samples_from_stream(io::SpanStream& stream, std::size_t count)
{
std::vector<int16_t> samples;
samples.reserve(count);
for (std::size_t i = 0; i < count; i++) {
for (std::size_t i = 0; i < count; i++)
{
samples.push_back(io::read_int16(stream));
}
return samples;
}
template <typename... Ts>
struct OverloadVisitor : Ts... {
using Ts::operator()...;
};
template <typename... Ts>
OverloadVisitor(Ts...) -> OverloadVisitor<Ts...>;
} // namespace
Wav::Wav() = default;
Wav::Wav(tcb::span<std::byte> data) {
Wav::Wav(tcb::span<std::byte> data)
{
io::SpanStream stream {data};
auto [magic, filesize] = parse_riff_header(stream);
if (magic != kMagicRIFF) {
if (magic != kMagicRIFF)
{
throw std::runtime_error("invalid RIFF magic");
}
if (io::remaining(stream) < filesize) {
if (io::remaining(stream) < filesize)
{
throw std::runtime_error("insufficient data in stream for RIFF's reported filesize");
}
const io::StreamSize riff_end = stream.seek(io::SeekFrom::kCurrent, 0) + filesize;
uint32_t type = io::read_uint32(stream);
if (type != kMagicWAVE) {
if (type != kMagicWAVE)
{
throw std::runtime_error("RIFF in stream is not a WAVE");
}
std::optional<FmtTag> read_fmt;
std::variant<std::vector<uint8_t>, std::vector<int16_t>> interleaved_samples;
while (stream.seek(io::SeekFrom::kCurrent, 0) < riff_end) {
while (stream.seek(io::SeekFrom::kCurrent, 0) < riff_end)
{
TagHeader tag_header {parse_tag_header(stream)};
if (io::remaining(stream) < tag_header.length) {
if (io::remaining(stream) < tag_header.length)
{
throw std::runtime_error("WAVE tag length exceeds stream length");
}
auto tag_visitor = OverloadVisitor {
[&](const FmtTag& fmt) {
if (read_fmt) {
auto tag_visitor = srb2::Overload {
[&](const FmtTag& fmt)
{
if (read_fmt)
{
throw std::runtime_error("WAVE has multiple 'fmt' tags");
}
if (fmt.format != kFormatPcm) {
if (fmt.format != kFormatPcm)
{
throw std::runtime_error("Unsupported WAVE format (only PCM is supported)");
}
read_fmt = fmt;
},
[&](const DataTag& data) {
if (!read_fmt) {
[&](const DataTag& data)
{
if (!read_fmt)
{
throw std::runtime_error("unable to read data tag because no fmt tag was read");
}
if (tag_header.length % read_fmt->bytes_per_sample != 0) {
if (tag_header.length % read_fmt->bytes_per_sample != 0)
{
throw std::runtime_error("data tag length not divisible by bytes_per_sample");
}
const std::size_t sample_count = tag_header.length / read_fmt->bytes_per_sample;
switch (read_fmt->bit_width) {
switch (read_fmt->bit_width)
{
case 8:
interleaved_samples = std::move(read_uint8_samples_from_stream(stream, sample_count));
break;
@ -207,7 +229,8 @@ Wav::Wav(tcb::span<std::byte> data) {
visit_tag(tag_visitor, stream, tag_header);
}
if (!read_fmt) {
if (!read_fmt)
{
throw std::runtime_error("WAVE did not have a fmt tag");
}
@ -216,27 +239,34 @@ Wav::Wav(tcb::span<std::byte> data) {
sample_rate_ = read_fmt->rate;
}
namespace {
namespace
{
template <typename T>
std::size_t read_samples(std::size_t channels,
std::size_t offset,
const std::vector<T>& samples,
tcb::span<audio::Sample<1>> buffer) noexcept {
std::size_t read_samples(
std::size_t channels,
std::size_t offset,
const std::vector<T>& samples,
tcb::span<audio::Sample<1>> buffer
) noexcept
{
const std::size_t offset_interleaved = offset * channels;
const std::size_t samples_size = samples.size();
const std::size_t buffer_size = buffer.size();
if (offset_interleaved >= samples_size) {
if (offset_interleaved >= samples_size)
{
return 0;
}
const std::size_t remainder = (samples_size - offset_interleaved) / channels;
const std::size_t samples_to_read = std::min(buffer_size, remainder);
for (std::size_t i = 0; i < samples_to_read; i++) {
for (std::size_t i = 0; i < samples_to_read; i++)
{
buffer[i].amplitudes[0] = 0.f;
for (std::size_t j = 0; j < channels; j++) {
for (std::size_t j = 0; j < channels; j++)
{
buffer[i].amplitudes[0] += audio::sample_to_float(samples[i * channels + j + offset_interleaved]);
}
buffer[i].amplitudes[0] /= static_cast<float>(channels);
@ -247,18 +277,20 @@ std::size_t read_samples(std::size_t channels,
} // namespace
std::size_t Wav::get_samples(std::size_t offset, tcb::span<audio::Sample<1>> buffer) const noexcept {
auto samples_visitor = OverloadVisitor {
std::size_t Wav::get_samples(std::size_t offset, tcb::span<audio::Sample<1>> buffer) const noexcept
{
auto samples_visitor = srb2::Overload {
[&](const std::vector<uint8_t>& samples) { return read_samples<uint8_t>(channels(), offset, samples, buffer); },
[&](const std::vector<int16_t>& samples) {
return read_samples<int16_t>(channels(), offset, samples, buffer);
}};
[&](const std::vector<int16_t>& samples)
{ return read_samples<int16_t>(channels(), offset, samples, buffer); }};
return std::visit(samples_visitor, interleaved_samples_);
}
std::size_t Wav::interleaved_length() const noexcept {
auto samples_visitor = OverloadVisitor {[](const std::vector<uint8_t>& samples) { return samples.size(); },
[](const std::vector<int16_t>& samples) { return samples.size(); }};
std::size_t Wav::interleaved_length() const noexcept
{
auto samples_visitor = srb2::Overload {
[](const std::vector<uint8_t>& samples) { return samples.size(); },
[](const std::vector<int16_t>& samples) { return samples.size(); }};
return std::visit(samples_visitor, interleaved_samples_);
}

View file

@ -21,9 +21,11 @@
#include "../io/streams.hpp"
#include "sample.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class Wav final {
class Wav final
{
std::variant<std::vector<uint8_t>, std::vector<int16_t>> interleaved_samples_;
std::size_t channels_ = 1;
std::size_t sample_rate_ = 44100;
@ -41,7 +43,8 @@ public:
};
template <typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Wav load_wav(I& stream) {
inline Wav load_wav(I& stream)
{
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Wav {data};
}

View file

@ -13,7 +13,8 @@ using namespace srb2;
using srb2::audio::WavPlayer;
WavPlayer::WavPlayer() : WavPlayer(audio::Wav {}) {
WavPlayer::WavPlayer() : WavPlayer(audio::Wav {})
{
}
WavPlayer::WavPlayer(const WavPlayer& rhs) = default;
@ -24,20 +25,25 @@ WavPlayer& WavPlayer::operator=(const WavPlayer& rhs) = default;
WavPlayer& WavPlayer::operator=(WavPlayer&& rhs) noexcept = default;
WavPlayer::WavPlayer(audio::Wav&& wav) noexcept : wav_(std::forward<Wav>(wav)), position_(0), looping_(false) {
WavPlayer::WavPlayer(audio::Wav&& wav) noexcept : wav_(std::forward<Wav>(wav)), position_(0), looping_(false)
{
}
std::size_t WavPlayer::generate(tcb::span<audio::Sample<1>> buffer) {
std::size_t WavPlayer::generate(tcb::span<audio::Sample<1>> buffer)
{
std::size_t samples_read = 0;
while (samples_read < buffer.size()) {
while (samples_read < buffer.size())
{
const std::size_t read_this_time = wav_.get_samples(position_, buffer.subspan(samples_read));
position_ += read_this_time;
samples_read += read_this_time;
if (position_ > wav_.length() && looping_) {
if (position_ > wav_.length() && looping_)
{
position_ = 0;
}
if (read_this_time == 0 && !looping_) {
if (read_this_time == 0 && !looping_)
{
break;
}
}

View file

@ -17,9 +17,11 @@
#include "source.hpp"
#include "wav.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class WavPlayer final : public Source<1> {
class WavPlayer final : public Source<1>
{
Wav wav_;
std::size_t position_;
bool looping_;

View file

@ -16,11 +16,14 @@
using namespace srb2;
using namespace srb2::audio;
XmpException::XmpException(int code) : code_(code) {
XmpException::XmpException(int code) : code_(code)
{
}
const char* XmpException::what() const noexcept {
switch (code_) {
const char* XmpException::what() const noexcept
{
switch (code_)
{
case -XMP_ERROR_INTERNAL:
return "XMP_ERROR_INTERNAL";
case -XMP_ERROR_FORMAT:
@ -41,23 +44,27 @@ const char* XmpException::what() const noexcept {
}
template <size_t C>
Xmp<C>::Xmp() : data_(), instance_(nullptr), module_loaded_(false), looping_(false) {
Xmp<C>::Xmp() : data_(), instance_(nullptr), module_loaded_(false), looping_(false)
{
}
template <size_t C>
Xmp<C>::Xmp(std::vector<std::byte> data)
: data_(std::move(data)), instance_(nullptr), module_loaded_(false), looping_(false) {
: data_(std::move(data)), instance_(nullptr), module_loaded_(false), looping_(false)
{
_init();
}
template <size_t C>
Xmp<C>::Xmp(tcb::span<std::byte> data)
: data_(data.begin(), data.end()), instance_(nullptr), module_loaded_(false), looping_(false) {
: data_(data.begin(), data.end()), instance_(nullptr), module_loaded_(false), looping_(false)
{
_init();
}
template <size_t C>
Xmp<C>::Xmp(Xmp<C>&& rhs) noexcept : Xmp<C>() {
Xmp<C>::Xmp(Xmp<C>&& rhs) noexcept : Xmp<C>()
{
std::swap(data_, rhs.data_);
std::swap(instance_, rhs.instance_);
std::swap(module_loaded_, rhs.module_loaded_);
@ -65,7 +72,8 @@ Xmp<C>::Xmp(Xmp<C>&& rhs) noexcept : Xmp<C>() {
}
template <size_t C>
Xmp<C>& Xmp<C>::operator=(Xmp<C>&& rhs) noexcept {
Xmp<C>& Xmp<C>::operator=(Xmp<C>&& rhs) noexcept
{
std::swap(data_, rhs.data_);
std::swap(instance_, rhs.instance_);
std::swap(module_loaded_, rhs.module_loaded_);
@ -75,15 +83,18 @@ Xmp<C>& Xmp<C>::operator=(Xmp<C>&& rhs) noexcept {
};
template <size_t C>
Xmp<C>::~Xmp() {
if (instance_) {
Xmp<C>::~Xmp()
{
if (instance_)
{
xmp_free_context(instance_);
instance_ = nullptr;
}
}
template <size_t C>
std::size_t Xmp<C>::play_buffer(tcb::span<std::array<int16_t, C>> buffer) {
std::size_t Xmp<C>::play_buffer(tcb::span<std::array<int16_t, C>> buffer)
{
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
@ -99,7 +110,8 @@ std::size_t Xmp<C>::play_buffer(tcb::span<std::array<int16_t, C>> buffer) {
}
template <size_t C>
void Xmp<C>::reset() {
void Xmp<C>::reset()
{
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
@ -107,7 +119,8 @@ void Xmp<C>::reset() {
}
template <size_t C>
float Xmp<C>::duration_seconds() const {
float Xmp<C>::duration_seconds() const
{
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
@ -117,7 +130,8 @@ float Xmp<C>::duration_seconds() const {
}
template <size_t C>
void Xmp<C>::seek(int position_ms) {
void Xmp<C>::seek(int position_ms)
{
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
@ -127,7 +141,8 @@ void Xmp<C>::seek(int position_ms) {
}
template <size_t C>
void Xmp<C>::_init() {
void Xmp<C>::_init()
{
if (instance_)
return;
@ -137,12 +152,14 @@ void Xmp<C>::_init() {
throw std::logic_error("Insufficient data from stream");
instance_ = xmp_create_context();
if (instance_ == nullptr) {
if (instance_ == nullptr)
{
throw std::bad_alloc();
}
int result = xmp_load_module_from_memory(instance_, data_.data(), data_.size());
if (result != 0) {
if (result != 0)
{
xmp_free_context(instance_);
instance_ = nullptr;
throw XmpException(result);
@ -150,11 +167,13 @@ void Xmp<C>::_init() {
module_loaded_ = true;
int flags = 0;
if constexpr (C == 1) {
if constexpr (C == 1)
{
flags |= XMP_FORMAT_MONO;
}
result = xmp_start_player(instance_, 44100, flags);
if (result != 0) {
if (result != 0)
{
xmp_release_module(instance_);
module_loaded_ = false;
xmp_free_context(instance_);

View file

@ -7,7 +7,7 @@
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_XMP_HPP__
#ifndef __SRB2_AUDIO_XMP_HPP__
#define __SRB2_AUDIO_XMP_HPP__
#include <array>
@ -22,9 +22,11 @@
#include "../io/streams.hpp"
namespace srb2::audio {
namespace srb2::audio
{
class XmpException : public std::exception {
class XmpException : public std::exception
{
int code_;
public:
@ -33,7 +35,8 @@ public:
};
template <size_t C>
class Xmp final {
class Xmp final
{
std::vector<std::byte> data_;
xmp_context instance_;
bool module_loaded_;
@ -68,7 +71,8 @@ extern template class Xmp<1>;
extern template class Xmp<2>;
template <size_t C, typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Xmp<C> load_xmp(I& stream) {
inline Xmp<C> load_xmp(I& stream)
{
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Xmp<C> {std::move(data)};
}

View file

@ -15,7 +15,8 @@ using namespace srb2;
using namespace srb2::audio;
template <size_t C>
XmpPlayer<C>::XmpPlayer(Xmp<C>&& xmp) : xmp_(std::move(xmp)), buf_() {
XmpPlayer<C>::XmpPlayer(Xmp<C>&& xmp) : xmp_(std::move(xmp)), buf_()
{
}
template <size_t C>
@ -28,14 +29,17 @@ template <size_t C>
XmpPlayer<C>::~XmpPlayer() = default;
template <size_t C>
std::size_t XmpPlayer<C>::generate(tcb::span<Sample<C>> buffer) {
std::size_t XmpPlayer<C>::generate(tcb::span<Sample<C>> buffer)
{
buf_.resize(buffer.size());
std::size_t read = xmp_.play_buffer(tcb::make_span(buf_));
buf_.resize(read);
std::size_t ret = std::min(buffer.size(), buf_.size());
for (std::size_t i = 0; i < ret; i++) {
for (std::size_t j = 0; j < C; j++) {
for (std::size_t i = 0; i < ret; i++)
{
for (std::size_t j = 0; j < C; j++)
{
buffer[i].amplitudes[j] = buf_[i][j] / 32768.f;
}
}
@ -44,12 +48,14 @@ std::size_t XmpPlayer<C>::generate(tcb::span<Sample<C>> buffer) {
}
template <size_t C>
float XmpPlayer<C>::duration_seconds() const {
float XmpPlayer<C>::duration_seconds() const
{
return xmp_.duration_seconds();
}
template <size_t C>
void XmpPlayer<C>::seek(float position_seconds) {
void XmpPlayer<C>::seek(float position_seconds)
{
xmp_.seek(static_cast<int>(std::round(position_seconds * 1000.f)));
}

View file

@ -13,10 +13,12 @@
#include "source.hpp"
#include "xmp.hpp"
namespace srb2::audio {
namespace srb2::audio
{
template <size_t C>
class XmpPlayer final : public Source<C> {
class XmpPlayer final : public Source<C>
{
Xmp<C> xmp_;
std::vector<std::array<int16_t, C>> buf_;

3
src/core/CMakeLists.txt Normal file
View file

@ -0,0 +1,3 @@
target_sources(SRB2SDL2 PRIVATE
static_vec.hpp
)

230
src/core/static_vec.hpp Normal file
View file

@ -0,0 +1,230 @@
#ifndef __SRB2_CORE_STATIC_VEC_HPP__
#define __SRB2_CORE_STATIC_VEC_HPP__
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <iterator>
#include <stdexcept>
#include <type_traits>
#include "../cxxutil.hpp"
namespace srb2
{
template <typename T, size_t Limit>
class StaticVec
{
std::array<T, Limit> arr_ {{}};
size_t size_ = 0;
public:
constexpr StaticVec() {}
StaticVec(const StaticVec& rhs)
{
for (size_t i = size_; i > 0; i--)
{
arr_[i] = T();
}
size_ = rhs.size();
for (size_t i = 0; i < size_; i++)
{
arr_[i] = rhs.arr_[i];
}
}
StaticVec(StaticVec&& rhs) noexcept(std::is_nothrow_move_assignable_v<T>)
{
for (size_t i = size_; i > 0; i--)
{
arr_[i] = T();
}
size_ = rhs.size();
for (size_t i = 0; i < size_; i++)
{
arr_[i] = std::move(rhs.arr_[i]);
}
while (rhs.size() > 0)
{
rhs.pop_back();
}
}
constexpr StaticVec(std::initializer_list<T> list) noexcept(std::is_nothrow_move_assignable_v<T>)
{
size_ = list.size();
size_t i = 0;
for (auto itr = list.begin(); itr != list.end(); itr++)
{
arr_[i] = *itr;
i++;
}
}
~StaticVec() = default;
StaticVec& operator=(const StaticVec& rhs)
{
for (size_t i = size_; i > 0; i--)
{
arr_[i] = T();
}
size_ = rhs.size();
for (size_t i = 0; i < size_; i++)
{
arr_[i] = rhs.arr_[i];
}
return *this;
}
StaticVec& operator=(StaticVec&& rhs) noexcept(std::is_nothrow_move_constructible_v<T>)
{
for (size_t i = size_; i > 0; i--)
{
arr_[i] = T();
}
size_ = rhs.size();
for (size_t i = 0; i < size_; i++)
{
arr_[i] = std::move(rhs.arr_[i]);
}
while (rhs.size() > 0)
{
rhs.pop_back();
}
return *this;
}
void push_back(const T& value) { arr_[size_++] = value; }
void pop_back() { arr_[size_--] = T(); }
void resize(size_t size, T value = T())
{
if (size >= Limit)
{
throw std::length_error("new size >= Capacity");
}
if (size == size_)
{
return;
}
else if (size < size_)
{
while (size_ > size)
{
pop_back();
}
}
else
{
while (size_ < size)
{
push_back(value);
}
}
}
constexpr T* begin() noexcept { return &arr_[0]; }
constexpr const T* begin() const noexcept { return cbegin(); }
constexpr const T* cbegin() const noexcept { return &arr_[0]; }
constexpr T* end() noexcept { return &arr_[size_]; }
constexpr const T* end() const noexcept { return cend(); }
constexpr const T* cend() const noexcept { return &arr_[size_]; }
constexpr std::reverse_iterator<T*> rbegin() noexcept { return &arr_[size_]; }
constexpr std::reverse_iterator<const T*> crbegin() const noexcept { return &arr_[size_]; }
constexpr std::reverse_iterator<T*> rend() noexcept { return &arr_[0]; }
constexpr std::reverse_iterator<const T*> crend() const noexcept { return &arr_[0]; }
constexpr bool empty() const noexcept { return size_ == 0; }
constexpr size_t size() const noexcept { return size_; }
constexpr size_t capacity() const noexcept { return Limit; }
constexpr size_t max_size() const noexcept { return Limit; }
constexpr T& operator[](size_t index) noexcept { return arr_[index]; }
T& at(size_t index)
{
if (index >= size_)
{
throw std::out_of_range("index >= size");
}
return this[index];
}
constexpr const T& operator[](size_t index) const noexcept { return arr_[index]; }
const T& at(size_t index) const
{
if (index >= size_)
{
throw std::out_of_range("index >= size");
}
return this[index];
}
T& front() { return *arr_[0]; }
T& back() { return *arr_[size_ - 1]; }
};
} // namespace srb2
template <typename T, size_t L1, size_t L2>
bool operator==(const srb2::StaticVec<T, L1>& lhs, const srb2::StaticVec<T, L2>& rhs)
{
const size_t size = lhs.size();
if (size != rhs.size())
{
return false;
}
for (size_t i = 0; i < lhs; i++)
{
if (rhs[i] != lhs[i])
{
return false;
}
}
return true;
}
template <typename T, size_t L1, size_t L2>
bool operator!=(const srb2::StaticVec<T, L1>& lhs, const srb2::StaticVec<T, L2>& rhs)
{
return !(lhs == rhs);
}
template <typename T, size_t Limit>
struct std::hash<srb2::StaticVec<T, Limit>>
{
uint64_t operator()(const srb2::StaticVec<T, Limit>& input) const
{
constexpr const uint64_t prime {0x00000100000001B3};
std::size_t ret {0xcbf29ce484222325};
for (auto itr = input.begin(); itr != input.end(); itr++)
{
ret = (ret * prime) ^ std::hash<T>(*itr);
}
return ret;
}
};
#endif // __SRB2_CORE_STATIC_VEC_HPP__

View file

@ -62,10 +62,12 @@ struct SourceLocation {
class IErrorAssertHandler {
public:
static void handle(const SourceLocation& source_location, const char* expression) {
I_Error("Assertion failed at %s:%u: %s != true",
source_location.file_name,
source_location.line_number,
expression);
I_Error(
"Assertion failed at %s:%u: %s != true",
source_location.file_name,
source_location.line_number,
expression
);
}
};
@ -102,8 +104,10 @@ class NotNull final {
T ptr_;
public:
static_assert(std::is_convertible_v<decltype(std::declval<T>() != nullptr), bool>,
"T is not comparable with nullptr_t");
static_assert(
std::is_convertible_v<decltype(std::declval<T>() != nullptr), bool>,
"T is not comparable with nullptr_t"
);
/// @brief Move-construct from the pointer value U, asserting that it is not null. Allows construction of a
/// NotNull<T> from any compatible pointer U, for example with polymorphic classes.
@ -147,6 +151,17 @@ public:
template <class T>
NotNull(T) -> NotNull<T>;
/// @brief Utility struct for combining several Callables (e.g. lambdas) into a single Callable with the call operator
/// overloaded. Use it to build a visitor for calling std::visit on variants.
/// @tparam ...Ts callable types
template <typename... Ts>
struct Overload : Ts... {
using Ts::operator()...;
};
template <typename... Ts>
Overload(Ts...) -> Overload<Ts...>;
} // namespace srb2
#endif // __SRB2_CXXUTIL_HPP__

View file

@ -1141,6 +1141,7 @@ static void IdentifyVersion(void)
#if defined(DEVELOP) && defined(UNLOCKTESTING)
D_AddFile(startupiwads, va(pandf,srb2waddir,UNLOCKNAME));
#endif
D_AddFile(startupiwads, va(pandf,srb2waddir,"shaders.pk3"));
////
#undef TEXTURESNAME
#undef MAPSNAME
@ -1466,6 +1467,7 @@ void D_SRB2Main(void)
#endif
#endif //ifndef DEVELOP
mainwads++; // shaders.pk3
// Do it before P_InitMapData because PNG patch
// conversion sometimes needs the palette

View file

@ -61,7 +61,7 @@ static boolean AddFileToSendQueue(INT32 node, const char *filename, UINT8 fileid
#ifdef HAVE_CURL
size_t curlwrite_data(void *ptr, size_t size, size_t nmemb, FILE *stream);
int curlprogress_callback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow);
int curlprogress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
#endif
// Sender structure
@ -123,8 +123,8 @@ static CURL *http_handle;
static CURLM *multi_handle;
boolean curl_running = false;
boolean curl_failedwebdownload = false;
static double curl_dlnow;
static double curl_dltotal;
static curl_off_t curl_dlnow;
static curl_off_t curl_dltotal;
static time_t curl_starttime;
INT32 curl_transfers = 0;
static int curl_runninghandles = 0;
@ -1763,7 +1763,7 @@ size_t curlwrite_data(void *ptr, size_t size, size_t nmemb, FILE *stream)
return written;
}
int curlprogress_callback(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow)
int curlprogress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
(void)clientp;
(void)ultotal;
@ -1802,7 +1802,11 @@ void CURLPrepareFile(const char* url, int dfilenum)
curl_easy_setopt(http_handle, CURLOPT_URL, va("%s/%s", url, curl_realname));
// Only allow HTTP and HTTPS
curl_easy_setopt(http_handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS);
#if LIBCURL_VERSION_MAJOR > 7 || (LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR >= 85)
curl_easy_setopt(http_handle, CURLOPT_PROTOCOLS_STR, "http,https");
#else
curl_easy_setopt(http_handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP|CURLPROTO_HTTPS); // deprecated in 7.85.0
#endif
curl_easy_setopt(http_handle, CURLOPT_USERAGENT, va("Ring Racers/v%d.%d", VERSION, SUBVERSION)); // Set user agent as some servers won't accept invalid user agents.
@ -1826,7 +1830,7 @@ void CURLPrepareFile(const char* url, int dfilenum)
curl_easy_setopt(http_handle, CURLOPT_WRITEDATA, curl_curfile->file);
curl_easy_setopt(http_handle, CURLOPT_WRITEFUNCTION, curlwrite_data);
curl_easy_setopt(http_handle, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(http_handle, CURLOPT_PROGRESSFUNCTION, curlprogress_callback);
curl_easy_setopt(http_handle, CURLOPT_XFERINFOFUNCTION, curlprogress_callback);
curl_curfile->status = FS_DOWNLOADING;
lastfilenum = dfilenum;

8
src/hwr2/CMakeLists.txt Normal file
View file

@ -0,0 +1,8 @@
target_sources(SRB2SDL2 PRIVATE
pass_imgui.cpp
pass_imgui.hpp
pass_software.cpp
pass_software.hpp
pass.cpp
pass.hpp
)

3
src/hwr2/pass.cpp Normal file
View file

@ -0,0 +1,3 @@
#include "pass.hpp"
srb2::hwr2::Pass::~Pass() = default;

35
src/hwr2/pass.hpp Normal file
View file

@ -0,0 +1,35 @@
#ifndef __SRB2_HWR2_PASS_HPP__
#define __SRB2_HWR2_PASS_HPP__
#include "../rhi/rhi.hpp"
namespace srb2::hwr2
{
/// @brief A rendering pass which performs logic during each phase of a frame render.
/// During rendering, all registered Pass's individual stages will be run together.
struct Pass {
virtual ~Pass();
/// @brief Perform rendering logic and create necessary GPU resources.
/// @param rhi
virtual void prepass(rhi::Rhi& rhi) = 0;
/// @brief Upload contents for needed GPU resources.
/// @param rhi
/// @param ctx
virtual void transfer(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx) = 0;
/// @brief Issue draw calls.
/// @param rhi
/// @param ctx
virtual void graphics(rhi::Rhi& rhi, rhi::Handle<rhi::GraphicsContext> ctx) = 0;
/// @brief Cleanup GPU resources. Transient resources should be cleaned up here.
/// @param rhi
virtual void postpass(rhi::Rhi& rhi) = 0;
};
} // namespace srb2::hwr2
#endif // __SRB2_HWR2_PASS_HPP__

255
src/hwr2/pass_imgui.cpp Normal file
View file

@ -0,0 +1,255 @@
#include "pass_imgui.hpp"
#include <imgui.h>
#include "../v_video.h"
using namespace srb2;
using namespace srb2::hwr2;
using namespace srb2::rhi;
static const PipelineDesc kPipelineDesc =
{
PipelineProgram::kUnshaded,
{
{
{sizeof(ImDrawVert)}
},
{
{VertexAttributeName::kPosition, 0, 0},
{VertexAttributeName::kTexCoord0, 0, 12},
{VertexAttributeName::kColor, 0, 24}
}
},
{{
{{UniformName::kProjection}},
{{UniformName::kModelView, UniformName::kTexCoord0Transform}}
}},
{{
SamplerName::kSampler0
}},
PipelineDepthAttachmentDesc {
PixelFormat::kDepth16,
CompareFunc::kAlways,
true
},
{
PixelFormat::kRGBA8,
BlendDesc {
BlendFactor::kSourceAlpha,
BlendFactor::kOneMinusSourceAlpha,
BlendFunction::kAdd,
BlendFactor::kOne,
BlendFactor::kOneMinusSourceAlpha,
BlendFunction::kAdd
},
{true, true, true, true}
},
PrimitiveType::kTriangles,
CullMode::kNone,
FaceWinding::kCounterClockwise,
{0.f, 0.f, 0.f, 1.f}
};
ImguiPass::~ImguiPass() = default;
void ImguiPass::prepass(Rhi& rhi)
{
if (!pipeline_)
{
pipeline_ = rhi.create_pipeline(kPipelineDesc);
}
ImGuiIO& io = ImGui::GetIO();
if (!font_atlas_)
{
unsigned char* pixels;
int width;
int height;
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
uint32_t uwidth = static_cast<uint32_t>(width);
uint32_t uheight = static_cast<uint32_t>(height);
font_atlas_ = rhi.create_texture({TextureFormat::kRGBA, uwidth, uheight});
io.Fonts->SetTexID(font_atlas_);
}
ImGui::Render();
ImDrawData* data = ImGui::GetDrawData();
ImVec2 clip_off(data->DisplayPos);
ImVec2 clip_scale(data->FramebufferScale);
tcb::span<ImDrawList*> draw_lists = tcb::span(data->CmdLists, data->CmdListsCount);
for (auto list : draw_lists)
{
Handle<Buffer> vbo = rhi.create_buffer(
{
static_cast<uint32_t>(list->VtxBuffer.size_in_bytes()),
BufferType::kVertexBuffer,
BufferUsage::kImmutable
}
);
Handle<Buffer> ibo = rhi.create_buffer(
{
static_cast<uint32_t>(list->IdxBuffer.size_in_bytes()),
BufferType::kIndexBuffer,
BufferUsage::kImmutable
}
);
DrawList hwr2_list;
hwr2_list.list = list;
hwr2_list.vbo = vbo;
hwr2_list.ibo = ibo;
for (auto& cmd : list->CmdBuffer)
{
if (cmd.UserCallback)
{
cmd.UserCallback(list, &cmd);
continue;
}
ImVec2 clip_min((cmd.ClipRect.x - clip_off.x) * clip_scale.x, (cmd.ClipRect.y - clip_off.y) * clip_scale.y);
ImVec2 clip_max((cmd.ClipRect.z - clip_off.x) * clip_scale.x, (cmd.ClipRect.w - clip_off.y) * clip_scale.y);
if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
{
continue;
}
DrawCmd draw_cmd;
ImTextureID tex_id = cmd.GetTexID();
draw_cmd.tex = tex_id;
draw_cmd.v_offset = cmd.VtxOffset;
draw_cmd.i_offset = cmd.IdxOffset;
draw_cmd.elems = cmd.ElemCount;
draw_cmd.clip =
{
static_cast<int32_t>(clip_min.x),
static_cast<int32_t>((data->DisplaySize.y * data->FramebufferScale.y) - clip_max.y),
static_cast<uint32_t>(clip_max.x - clip_min.x),
static_cast<uint32_t>(clip_max.y - clip_min.y)
};
hwr2_list.cmds.push_back(std::move(draw_cmd));
}
draw_lists_.push_back(std::move(hwr2_list));
}
}
void ImguiPass::transfer(Rhi& rhi, Handle<TransferContext> ctx)
{
ImGuiIO& io = ImGui::GetIO();
{
unsigned char* pixels;
int width, height;
io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height);
rhi.update_texture(
ctx,
font_atlas_,
{0, 0, static_cast<uint32_t>(width), static_cast<uint32_t>(height)},
rhi::PixelFormat::kRGBA8,
tcb::as_bytes(tcb::span(pixels, static_cast<size_t>(width * height * 4)))
);
}
for (auto& draw_list : draw_lists_)
{
Handle<Buffer> vbo = draw_list.vbo;
Handle<Buffer> ibo = draw_list.ibo;
ImDrawList* im_list = static_cast<ImDrawList*>(draw_list.list);
for (auto& vtx : im_list->VtxBuffer)
{
vtx.pos.z = 0.f;
vtx.colf[0] = ((vtx.col & 0xFF) >> 0) / 255.f;
vtx.colf[1] = ((vtx.col & 0xFF00) >> 8) / 255.f;
vtx.colf[2] = ((vtx.col & 0xFF0000) >> 16) / 255.f;
vtx.colf[3] = ((vtx.col & 0xFF000000) >> 24) / 255.f;
}
tcb::span<ImDrawVert> vert_span = tcb::span(im_list->VtxBuffer.Data, im_list->VtxBuffer.size());
rhi.update_buffer_contents(ctx, vbo, 0, tcb::as_bytes(vert_span));
tcb::span<ImDrawIdx> index_span = tcb::span(im_list->IdxBuffer.Data, im_list->IdxBuffer.size());
rhi.update_buffer_contents(ctx, ibo, 0, tcb::as_bytes(index_span));
// Uniform sets
std::array<UniformVariant, 1> g1_uniforms =
{{
// Projection
std::array<std::array<float, 4>, 4>
{{
{2.f / vid.realwidth, 0.f, 0.f, 0.f},
{0.f, 2.f / vid.realheight, 0.f, 0.f},
{0.f, 0.f, 1.f, 0.f},
{-1.f, 1.f, 0.f, 1.f}
}},
}};
std::array<UniformVariant, 2> g2_uniforms =
{{
// ModelView
std::array<std::array<float, 4>, 4>
{{
{1.f, 0.f, 0.f, 0.f},
{0.f, -1.f, 0.f, 0.f},
{0.f, 0.f, 1.f, 0.f},
{0.f, 0, 0.f, 1.f}
}},
// Texcoord0 Transform
std::array<std::array<float, 3>, 3>
{{
{1.f, 0.f, 0.f},
{0.f, 1.f, 0.f},
{0.f, 0.f, 1.f}
}}
}};
Handle<UniformSet> us_1 = rhi.create_uniform_set(ctx, {g1_uniforms});
Handle<UniformSet> us_2 = rhi.create_uniform_set(ctx, {g2_uniforms});
draw_list.us_1 = us_1;
draw_list.us_2 = us_2;
for (auto& draw_cmd : draw_list.cmds)
{
// Binding set
std::array<rhi::VertexAttributeBufferBinding, 1> vbos = {{0, vbo}};
std::array<rhi::TextureBinding, 1> tbs = {{{rhi::SamplerName::kSampler0, draw_cmd.tex}}};
rhi::Handle<rhi::BindingSet> binding_set = rhi.create_binding_set(ctx, pipeline_, {vbos, tbs});
draw_cmd.binding_set = binding_set;
}
}
}
void ImguiPass::graphics(Rhi& rhi, Handle<GraphicsContext> ctx)
{
rhi.begin_default_render_pass(ctx, false);
rhi.bind_pipeline(ctx, pipeline_);
for (auto& draw_list : draw_lists_)
{
rhi.bind_uniform_set(ctx, 0, draw_list.us_1);
rhi.bind_uniform_set(ctx, 1, draw_list.us_2);
for (auto& cmd : draw_list.cmds)
{
rhi.bind_binding_set(ctx, cmd.binding_set);
rhi.bind_index_buffer(ctx, draw_list.ibo);
rhi.set_scissor(ctx, cmd.clip);
rhi.draw_indexed(ctx, cmd.elems, cmd.i_offset);
}
}
rhi.end_render_pass(ctx);
}
void ImguiPass::postpass(Rhi& rhi)
{
for (auto& list : draw_lists_)
{
rhi.destroy_buffer(list.vbo);
rhi.destroy_buffer(list.ibo);
}
draw_lists_.clear();
}

52
src/hwr2/pass_imgui.hpp Normal file
View file

@ -0,0 +1,52 @@
#ifndef __SRB2_HWR2_PASS_IMGUI_HPP__
#define __SRB2_HWR2_PASS_IMGUI_HPP__
#include <vector>
#include "../rhi/rhi.hpp"
#include "pass.hpp"
namespace srb2::hwr2
{
class ImguiPass : public Pass
{
struct DrawCmd
{
rhi::Handle<rhi::Texture> tex;
uint32_t v_offset;
uint32_t elems;
uint32_t i_offset;
rhi::Rect clip;
rhi::Handle<rhi::BindingSet> binding_set;
};
struct DrawList
{
void* list;
rhi::Handle<rhi::Buffer> vbo;
rhi::Handle<rhi::Buffer> ibo;
rhi::Handle<rhi::UniformSet> us_1;
rhi::Handle<rhi::UniformSet> us_2;
std::vector<DrawCmd> cmds;
};
rhi::Handle<rhi::Pipeline> pipeline_;
rhi::Handle<rhi::Texture> font_atlas_;
std::vector<DrawList> draw_lists_;
public:
virtual ~ImguiPass();
virtual void prepass(rhi::Rhi& rhi) override;
virtual void transfer(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx) override;
virtual void graphics(rhi::Rhi& rhi, rhi::Handle<rhi::GraphicsContext> ctx) override;
virtual void postpass(rhi::Rhi& rhi) override;
};
} // namespace srb2::hwr2
#endif // __SRB2_HWR2_PASS_IMGUI_HPP__

284
src/hwr2/pass_software.cpp Normal file
View file

@ -0,0 +1,284 @@
#include "pass_software.hpp"
#include <optional>
#include <tcb/span.hpp>
#include "../cxxutil.hpp"
#include "../d_netcmd.h"
#ifdef HAVE_DISCORDRPC
#include "../discord.h"
#endif
#include "../doomstat.h"
#include "../st_stuff.h"
#include "../s_sound.h"
#include "../v_video.h"
using namespace srb2;
using namespace srb2::hwr2;
using namespace srb2::rhi;
SoftwareBlitPass::~SoftwareBlitPass() = default;
namespace
{
struct SwBlitVertex
{
float x = 0.f;
float y = 0.f;
float z = 0.f;
float u = 0.f;
float v = 0.f;
};
} // namespace
static const SwBlitVertex kVerts[] =
{
{-.5f, -.5f, 0.f, 0.f, 0.f},
{.5f, -.5f, 0.f, 1.f, 0.f},
{-.5f, .5f, 0.f, 0.f, 1.f},
{.5f, .5f, 0.f, 1.f, 1.f}
};
static const uint16_t kIndices[] = {0, 1, 2, 1, 3, 2};
static const PipelineDesc kPipelineDescription =
{
PipelineProgram::kUnshadedPaletted,
{
{
{sizeof(SwBlitVertex)}
},
{
{VertexAttributeName::kPosition, 0, 0},
{VertexAttributeName::kTexCoord0, 0, 12}
}
},
{{
{{UniformName::kProjection}},
{{UniformName::kModelView, UniformName::kTexCoord0Transform}}
}},
{{
// R8 index texture
SamplerName::kSampler0,
// 256x1 palette texture
SamplerName::kSampler1
}},
std::nullopt,
{
PixelFormat::kRGBA8,
std::nullopt,
{true, true, true, true}
},
PrimitiveType::kTriangles,
CullMode::kNone,
FaceWinding::kCounterClockwise,
{0.f, 0.f, 0.f, 1.f}
};
static uint32_t next_pow_of_2(uint32_t in)
{
in--;
in |= in >> 1;
in |= in >> 2;
in |= in >> 4;
in |= in >> 8;
in |= in >> 16;
in++;
return in;
}
static void temp_legacy_finishupdate_draws()
{
SCR_CalculateFPS();
if (st_overlay)
{
if (cv_ticrate.value)
SCR_DisplayTicRate();
if (cv_showping.value && netgame &&
( consoleplayer != serverplayer || ! server_lagless ))
{
if (server_lagless)
{
if (consoleplayer != serverplayer)
SCR_DisplayLocalPing();
}
else
{
for (
int player = 1;
player < MAXPLAYERS;
player++
){
if (D_IsPlayerHumanAndGaming(player))
{
SCR_DisplayLocalPing();
break;
}
}
}
}
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
SCR_DisplayLocalPing();
}
if (marathonmode)
SCR_DisplayMarathonInfo();
// draw captions if enabled
if (cv_closedcaptioning.value)
SCR_ClosedCaptions();
#ifdef HAVE_DISCORDRPC
if (discordRequestList != NULL)
ST_AskToJoinEnvelope();
#endif
}
void SoftwareBlitPass::prepass(Rhi& rhi)
{
if (!pipeline_)
{
pipeline_ = rhi.create_pipeline(kPipelineDescription);
}
if (!quad_vbo_)
{
quad_vbo_ = rhi.create_buffer({sizeof(kVerts), BufferType::kVertexBuffer, BufferUsage::kImmutable});
quad_vbo_needs_upload_ = true;
}
if (!quad_ibo_)
{
quad_ibo_ = rhi.create_buffer({sizeof(kIndices), BufferType::kIndexBuffer, BufferUsage::kImmutable});
quad_ibo_needs_upload_ = true;
}
temp_legacy_finishupdate_draws();
uint32_t vid_width = static_cast<uint32_t>(vid.width);
uint32_t vid_height = static_cast<uint32_t>(vid.height);
if (screen_tex_ && (screen_tex_width_ < vid_width || screen_tex_height_ < vid_height))
{
rhi.destroy_texture(screen_tex_);
screen_tex_ = kNullHandle;
}
if (!screen_tex_)
{
screen_tex_width_ = next_pow_of_2(vid_width);
screen_tex_height_ = next_pow_of_2(vid_height);
screen_tex_ = rhi.create_texture({TextureFormat::kLuminance, screen_tex_width_, screen_tex_height_});
}
if (!palette_tex_)
{
palette_tex_ = rhi.create_texture({TextureFormat::kRGBA, 256, 1});
}
}
void SoftwareBlitPass::upload_screen(Rhi& rhi, Handle<TransferContext> ctx)
{
rhi::Rect screen_rect = {
0,
0,
static_cast<uint32_t>(vid.width),
static_cast<uint32_t>(vid.height)
};
tcb::span<uint8_t> screen_span = tcb::span(vid.buffer, static_cast<size_t>(vid.width * vid.height));
rhi.update_texture(ctx, screen_tex_, screen_rect, rhi::PixelFormat::kR8, tcb::as_bytes(screen_span));
}
void SoftwareBlitPass::upload_palette(Rhi& rhi, Handle<TransferContext> ctx)
{
// Unfortunately, pMasterPalette must be swizzled to get a linear layout.
// Maybe some adjustments to palette storage can make this a straight upload.
std::array<byteColor_t, 256> palette_32;
for (size_t i = 0; i < 256; i++)
{
palette_32[i] = pMasterPalette[i].s;
}
rhi.update_texture(ctx, palette_tex_, {0, 0, 256, 1}, rhi::PixelFormat::kRGBA8, tcb::as_bytes(tcb::span(palette_32)));
}
void SoftwareBlitPass::transfer(Rhi& rhi, Handle<TransferContext> ctx)
{
if (quad_vbo_needs_upload_ && quad_vbo_)
{
rhi.update_buffer_contents(ctx, quad_vbo_, 0, tcb::as_bytes(tcb::span(kVerts)));
quad_vbo_needs_upload_ = false;
}
if (quad_ibo_needs_upload_ && quad_ibo_)
{
rhi.update_buffer_contents(ctx, quad_ibo_, 0, tcb::as_bytes(tcb::span(kIndices)));
quad_ibo_needs_upload_ = false;
}
upload_screen(rhi, ctx);
upload_palette(rhi, ctx);
// Calculate aspect ratio for black borders
float aspect = static_cast<float>(vid.width) / static_cast<float>(vid.height);
float real_aspect = static_cast<float>(vid.realwidth) / static_cast<float>(vid.realheight);
bool taller = aspect > real_aspect;
std::array<rhi::UniformVariant, 1> g1_uniforms = {{
// Projection
std::array<std::array<float, 4>, 4> {{
{taller ? 1.f : 1.f / real_aspect, 0.f, 0.f, 0.f},
{0.f, taller ? -1.f / (1.f / real_aspect) : -1.f, 0.f, 0.f},
{0.f, 0.f, 1.f, 0.f},
{0.f, 0.f, 0.f, 1.f}
}},
}};
std::array<rhi::UniformVariant, 2> g2_uniforms =
{{
// ModelView
std::array<std::array<float, 4>, 4>
{{
{taller ? 2.f : 2.f * aspect, 0.f, 0.f, 0.f},
{0.f, taller ? 2.f * (1.f / aspect) : 2.f, 0.f, 0.f},
{0.f, 0.f, 1.f, 0.f},
{0.f, 0.f, 0.f, 1.f}
}},
// Texcoord0 Transform
std::array<std::array<float, 3>, 3>
{{
{vid.width / static_cast<float>(screen_tex_width_), 0.f, 0.f},
{0.f, vid.height / static_cast<float>(screen_tex_height_), 0.f},
{0.f, 0.f, 1.f}
}}
}};
uniform_sets_[0] = rhi.create_uniform_set(ctx, {g1_uniforms});
uniform_sets_[1] = rhi.create_uniform_set(ctx, {g2_uniforms});
std::array<rhi::VertexAttributeBufferBinding, 1> vbs = {{{0, quad_vbo_}}};
std::array<rhi::TextureBinding, 2> tbs = {{
{rhi::SamplerName::kSampler0, screen_tex_},
{rhi::SamplerName::kSampler1, palette_tex_}
}};
binding_set_ = rhi.create_binding_set(ctx, pipeline_, {vbs, tbs});
}
void SoftwareBlitPass::graphics(Rhi& rhi, Handle<GraphicsContext> ctx)
{
rhi.begin_default_render_pass(ctx, true);
rhi.bind_pipeline(ctx, pipeline_);
rhi.bind_uniform_set(ctx, 0, uniform_sets_[0]);
rhi.bind_uniform_set(ctx, 1, uniform_sets_[1]);
rhi.bind_binding_set(ctx, binding_set_);
rhi.bind_index_buffer(ctx, quad_ibo_);
rhi.draw_indexed(ctx, 6, 0);
rhi.end_render_pass(ctx);
}
void SoftwareBlitPass::postpass(Rhi& rhi)
{
// no-op
}

View file

@ -0,0 +1,44 @@
#ifndef __SRB2_HWR2_PASS_SOFTWARE_HPP__
#define __SRB2_HWR2_PASS_SOFTWARE_HPP__
#include <array>
#include "../rhi/rhi.hpp"
#include "pass.hpp"
namespace srb2::hwr2
{
class SoftwareBlitPass : public Pass
{
rhi::Handle<rhi::Pipeline> pipeline_;
rhi::Handle<rhi::Texture> screen_tex_;
rhi::Handle<rhi::Texture> palette_tex_;
rhi::Handle<rhi::Buffer> quad_vbo_;
rhi::Handle<rhi::Buffer> quad_ibo_;
std::array<rhi::Handle<rhi::UniformSet>, 2> uniform_sets_;
rhi::Handle<rhi::BindingSet> binding_set_;
uint32_t screen_tex_width_ = 0;
uint32_t screen_tex_height_ = 0;
bool quad_vbo_needs_upload_ = false;
bool quad_ibo_needs_upload_ = false;
void upload_screen(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx);
void upload_palette(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx);
public:
virtual ~SoftwareBlitPass();
virtual void prepass(rhi::Rhi& rhi) override;
virtual void transfer(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx) override;
virtual void graphics(rhi::Rhi& rhi, rhi::Handle<rhi::GraphicsContext> ctx) override;
virtual void postpass(rhi::Rhi& rhi) override;
};
} // namespace srb2::hwr2
#endif // __SRB2_HWR2_PASS_SOFTWARE_HPP__

View file

@ -17,6 +17,17 @@
#include "doomtype.h"
#ifdef __cplusplus
#include "rhi/rhi.hpp"
namespace srb2::sys {
extern rhi::Handle<rhi::Rhi> g_current_rhi;
rhi::Rhi* get_rhi(rhi::Handle<rhi::Rhi> handle);
} // namespace
extern "C" {
#endif
@ -44,10 +55,6 @@ extern rendermode_t rendermode;
*/
extern rendermode_t chosenrendermode;
/** \brief use highcolor modes if true
*/
extern boolean highcolor;
/** \brief setup video mode
*/
void I_StartupGraphics(void);

171
src/i_video_common.cpp Normal file
View file

@ -0,0 +1,171 @@
#include "i_video.h"
#include <algorithm>
#include <array>
#include <vector>
#include <imgui.h>
#include "cxxutil.hpp"
#include "hwr2/pass_imgui.hpp"
#include "hwr2/pass_software.hpp"
#include "v_video.h"
// KILL THIS WHEN WE KILL OLD OGL SUPPORT PLEASE
#include "sdl/ogl_sdl.h"
#include "st_stuff.h" // kill
#include "d_netcmd.h" // kill
#include "doomstat.h" // kill
#include "s_sound.h" // kill
#include "discord.h" // kill
using namespace srb2;
using namespace srb2::hwr2;
using namespace srb2::rhi;
static SoftwareBlitPass g_sw_pass;
static ImguiPass g_imgui_pass;
Handle<Rhi> srb2::sys::g_current_rhi = kNullHandle;
static bool rhi_changed()
{
return false;
}
#ifdef HWRENDER
static void finish_legacy_ogl_update()
{
int player;
SCR_CalculateFPS();
if (st_overlay)
{
if (cv_ticrate.value)
SCR_DisplayTicRate();
if (cv_showping.value && netgame &&
( consoleplayer != serverplayer || ! server_lagless ))
{
if (server_lagless)
{
if (consoleplayer != serverplayer)
SCR_DisplayLocalPing();
}
else
{
for (
player = 1;
player < MAXPLAYERS;
player++
){
if (D_IsPlayerHumanAndGaming(player))
{
SCR_DisplayLocalPing();
break;
}
}
}
}
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
SCR_DisplayLocalPing();
}
if (marathonmode)
SCR_DisplayMarathonInfo();
// draw captions if enabled
if (cv_closedcaptioning.value)
SCR_ClosedCaptions();
#ifdef HAVE_DISCORDRPC
if (discordRequestList != NULL)
ST_AskToJoinEnvelope();
#endif
OglSdlFinishUpdate(cv_vidwait.value);
}
#endif
void I_FinishUpdate(void)
{
if (rendermode == render_none)
{
return;
}
#ifdef HWRENDER
if (rendermode == render_opengl)
{
finish_legacy_ogl_update();
return;
}
#endif
// TODO move this to srb2loop
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize.x = vid.realwidth;
io.DisplaySize.y = vid.realheight;
ImGui::NewFrame();
if (rhi_changed())
{
// reinitialize passes
g_sw_pass = SoftwareBlitPass();
g_imgui_pass = ImguiPass();
}
rhi::Rhi* rhi = sys::get_rhi(sys::g_current_rhi);
if (rhi == nullptr)
{
// ???
return;
}
// Prepare phase
if (rendermode == render_soft)
{
g_sw_pass.prepass(*rhi);
}
g_imgui_pass.prepass(*rhi);
// Transfer phase
Handle<TransferContext> tc;
tc = rhi->begin_transfer();
if (rendermode == render_soft)
{
g_sw_pass.transfer(*rhi, tc);
}
g_imgui_pass.transfer(*rhi, tc);
rhi->end_transfer(tc);
// Graphics phase
Handle<GraphicsContext> gc;
gc = rhi->begin_graphics();
// Standard drawing passes...
if (rendermode == render_soft)
{
g_sw_pass.graphics(*rhi, gc);
}
g_imgui_pass.graphics(*rhi, gc);
rhi->end_graphics(gc);
// Postpass phase
if (rendermode == render_soft)
{
g_sw_pass.postpass(*rhi);
}
g_imgui_pass.postpass(*rhi);
// Present
rhi->present();
rhi->finish();
}

View file

@ -315,7 +315,8 @@ void K_HandleFollower(player_t *player)
// don't do anything if we can't have a follower to begin with.
// (It gets removed under those conditions)
if (player->spectator || player->followerskin < 0)
if (player->spectator || player->followerskin < 0
|| player->mo == NULL || P_MobjWasRemoved(player->mo))
{
if (player->follower)
{
@ -377,12 +378,15 @@ void K_HandleFollower(player_t *player)
// Set follower colour
color = K_GetEffectiveFollowerColor(player->followercolor, player->skincolor);
if (player->follower == NULL) // follower doesn't exist / isn't valid
if (player->follower == NULL || P_MobjWasRemoved(player->follower)) // follower doesn't exist / isn't valid
{
//CONS_Printf("Spawning follower...\n");
// so let's spawn one!
P_SetTarget(&player->follower, P_SpawnMobj(sx, sy, sz, MT_FOLLOWER));
if (player->follower == NULL)
return;
K_UpdateFollowerState(player->follower, fl.idlestate, FOLLOWERSTATE_IDLE);
P_SetTarget(&player->follower->target, player->mo); // we need that to know when we need to disappear
@ -402,11 +406,9 @@ void K_HandleFollower(player_t *player)
}
else // follower exists, woo!
{
// Safety net (2)
if (P_MobjWasRemoved(player->follower))
if (player->follower->hitlag != 0)
{
P_SetTarget(&player->follower, NULL); // Remove this and respawn one, don't crash the game if Lua decides to P_RemoveMobj this thing.
// Don't update frames in hitlag
return;
}
@ -625,4 +627,11 @@ void K_HandleFollower(player_t *player)
K_UpdateFollowerState(player->follower, fl.idlestate, FOLLOWERSTATE_IDLE);
}
}
if (player->mo->hitlag)
{
player->follower->hitlag = player->mo->hitlag;
player->follower->eflags |= (player->mo->eflags & MFE_DAMAGEHITLAG);
return;
}
}

View file

@ -3386,7 +3386,7 @@ fixed_t K_GetNewSpeed(player_t *player)
if (player->curshield == KSHIELD_TOP)
{
p_speed = 11 * p_speed / 10;
p_speed = 15 * p_speed / 10;
}
if (K_PlayerUsesBotMovement(player) == true && player->botvars.rubberband > 0)
@ -9873,7 +9873,9 @@ void K_AdjustPlayerFriction(player_t *player)
player->mo->friction += ((FRACUNIT - prevfriction) / greasetics) * player->tiregrease;
}
if (player->curshield == KSHIELD_TOP)
// Less friction on Top unless grinding
if (player->curshield == KSHIELD_TOP &&
K_GetForwardMove(player) > 0)
{
player->mo->friction += 1024;
}

View file

@ -556,6 +556,7 @@ extern boolean menuwipe;
extern consvar_t cv_showfocuslost;
extern consvar_t cv_chooseskin, cv_serversort, cv_menujam_update;
extern consvar_t cv_autorecord;
void M_SetMenuDelay(UINT8 i);
@ -569,6 +570,17 @@ void M_MapMenuControls(event_t *ev);
boolean M_Responder(event_t *ev);
boolean M_MenuButtonPressed(UINT8 pid, UINT32 bt);
boolean M_MenuButtonHeld(UINT8 pid, UINT32 bt);
boolean M_ChangeStringCvar(INT32 choice);
boolean M_NextOpt(void);
boolean M_PrevOpt(void);
boolean M_MenuConfirmPressed(UINT8 pid);
boolean M_MenuBackPressed(UINT8 pid);
boolean M_MenuExtraPressed(UINT8 pid);
boolean M_MenuExtraHeld(UINT8 pid);
void M_StartControlPanel(void);
void M_ClearMenus(boolean callexitmenufunc);
void M_SelectableClearMenus(INT32 choice);
@ -577,6 +589,8 @@ void M_GoBack(INT32 choice);
void M_Ticker(void);
void M_Init(void);
void M_MenuTypingInput(INT32 key);
extern menu_t MessageDef;
void M_StartMessage(const char *string, void *routine, menumessagetype_t itemtype);
void M_StopMessage(INT32 choice);
@ -679,6 +693,7 @@ extern consvar_t *setup_playercvars[MAXSPLITSCREENPLAYERS][SPLITCV_MAX];
void M_CharacterSelectInit(void);
void M_CharacterSelect(INT32 choice);
void M_SetupReadyExplosions(boolean charsel, UINT16 basex, UINT16 basey, UINT16 color);
boolean M_CharacterSelectHandler(INT32 choice);
void M_CharacterSelectTick(void);
boolean M_CharacterSelectQuit(void);
@ -723,6 +738,8 @@ boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch);
UINT16 M_CountLevelsToShowInList(levelsearch_t *levelsearch);
UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch);
UINT16 M_GetNextLevelInList(UINT16 mapnum, UINT8 *i, levelsearch_t *levelsearch);
void M_LevelSelectScrollDest(void);
boolean M_LevelListFromGametype(INT16 gt);
void M_LevelSelectInit(INT32 choice);
void M_CupSelectHandler(INT32 choice);
@ -730,6 +747,8 @@ void M_CupSelectTick(void);
void M_LevelSelectHandler(INT32 choice);
void M_LevelSelectTick(void);
void M_LevelSelected(INT16 add);
// dummy consvars for GP & match race setup
extern consvar_t cv_dummygpdifficulty;
extern consvar_t cv_dummykartspeed;
@ -916,6 +935,7 @@ void M_CheckProfileData(INT32 choice); // check if we have profiles.
// profile selection menu
void M_ProfileSelectInit(INT32 choice);
void M_FirstPickProfile(INT32 c);
void M_HandleProfileSelect(INT32 ch);
// profile edition

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -661,6 +661,42 @@ static void K_DropDashWait(player_t *player)
}
/*--------------------------------------------------
static boolean K_CanDropDash(player_t *player)
Checks if you can use the Drop Dash maneuver.
Input Arguments:-
player - Player to check.
Return:-
Whether a Drop Dash should be allowed.
--------------------------------------------------*/
static boolean K_CanDropDash(player_t *player)
{
const UINT16 buttons = K_GetKartButtons(player);
if (!(buttons & BT_ACCELERATE))
{
return false;
}
// Since we're letting players spin out on respawn, don't let them charge a dropdash in this state. (It wouldn't work anyway)
if (player->spinouttimer)
{
return false;
}
// Garden Top is overpowered enough
if (player->curshield == KSHIELD_TOP)
{
return false;
}
return true;
}
/*--------------------------------------------------
static void K_HandleDropDash(player_t *player)
@ -699,7 +735,7 @@ static void K_HandleDropDash(player_t *player)
// The old behavior was stupid and prone to accidental usage.
// Let's rip off Mania instead, and turn this into a Drop Dash!
if ((buttons & BT_ACCELERATE) && !player->spinouttimer) // Since we're letting players spin out on respawn, don't let them charge a dropdash in this state. (It wouldn't work anyway)
if (K_CanDropDash(player))
{
player->respawn.dropdash++;
}

View file

@ -966,26 +966,13 @@ void K_HandleFootstepParticles(mobj_t *mo)
return;
}
if (!(mo->flags & MF_APPLYTERRAIN))
if (!(mo->flags & MF_APPLYTERRAIN) || mo->terrain == NULL)
{
// No TERRAIN effects for this object.
return;
}
if (mo->terrain == NULL || mo->terrain->footstepID == SIZE_MAX)
{
// If no terrain, check for offroad.
// If we're in offroad, use the default particle.
if (mo->player != NULL && mo->player->boostpower < FRACUNIT)
{
fs = K_GetFootstepByIndex(defaultOffroadFootstep);
}
}
else
{
fs = K_GetFootstepByIndex(mo->terrain->footstepID);
}
fs = K_GetFootstepByIndex(mo->terrain->footstepID);
if (fs == NULL || fs->mobjType == MT_NULL || fs->frequency <= 0)
{

43
src/menus/CMakeLists.txt Normal file
View file

@ -0,0 +1,43 @@
target_sources(SRB2SDL2 PRIVATE
extras-1.c
extras-addons.c
extras-challenges.c
extras-replay-hut.c
extras-statistics.c
main-1.c
main-profile-select.c
options-1.c
options-data-1.c
options-data-addons.c
options-data-discord.c
options-data-erase-1.c
options-data-erase-profile.c
options-data-replays.c
options-data-screenshots.c
options-gameplay-1.c
options-gameplay-item-toggles.c
options-hud-1.c
options-hud-online.c
options-profiles-1.c
options-profiles-edit-1.c
options-profiles-edit-controls.c
options-server-1.c
options-server-advanced.c
options-sound.c
options-video-1.c
options-video-gl.c
options-video-modes.c
play-1.c
play-char-select.c
play-local-1.c
play-local-race-1.c
play-local-race-difficulty.c
play-local-race-time-attack.c
play-online-1.c
play-online-host.c
play-online-join-ip.c
play-online-room-select.c
play-online-server-browser.c
)
add_subdirectory(transient)

139
src/menus/extras-1.c Normal file
View file

@ -0,0 +1,139 @@
/// \file menus/extras-1.c
/// \brief Extras Menu
#include "../k_menu.h"
#include "../s_sound.h"
menuitem_t EXTRAS_Main[] =
{
{IT_STRING | IT_CALL, "Addons", "Add files to customize your experience.",
NULL, {.routine = M_Addons}, 0, 0},
{IT_STRING | IT_CALL, "Challenges", "View the requirements for some of the secret content you can unlock!",
NULL, {.routine = M_Challenges}, 0, 0},
{IT_STRING | IT_CALL, "Replay Hut", "Play the replays you've saved throughout your many races & battles!",
NULL, {.routine = M_ReplayHut}, 0, 0},
{IT_STRING | IT_CALL, "Statistics", "Look back on some of your greatest achievements such as your playtime and wins!",
NULL, {.routine = M_Statistics}, 0, 0},
};
// the extras menu essentially reuses the options menu stuff
menu_t EXTRAS_MainDef = {
sizeof (EXTRAS_Main) / sizeof (menuitem_t),
&MainDef,
0,
EXTRAS_Main,
0, 0,
0, 0,
2, 5,
M_DrawExtras,
M_ExtrasTick,
NULL,
NULL,
M_ExtrasInputs
};
// Extras menu;
// this is copypasted from the options menu but all of these are different functions in case we ever want it to look more unique
struct extrasmenu_s extrasmenu;
void M_InitExtras(INT32 choice)
{
(void)choice;
extrasmenu.ticker = 0;
extrasmenu.offset = 0;
extrasmenu.extx = 0;
extrasmenu.exty = 0;
extrasmenu.textx = 0;
extrasmenu.texty = 0;
M_SetupNextMenu(&EXTRAS_MainDef, false);
}
// For statistics, will maybe remain unused for a while
boolean M_ExtrasQuit(void)
{
extrasmenu.textx = 140-1;
extrasmenu.texty = 70+1;
return true; // Always allow quitting, duh.
}
void M_ExtrasTick(void)
{
extrasmenu.offset /= 2;
extrasmenu.ticker++;
extrasmenu.extx += (extrasmenu.textx - extrasmenu.extx)/2;
extrasmenu.exty += (extrasmenu.texty - extrasmenu.exty)/2;
if (abs(extrasmenu.extx - extrasmenu.exty) < 2)
{
extrasmenu.extx = extrasmenu.textx;
extrasmenu.exty = extrasmenu.texty; // Avoid awkward 1 px errors.
}
// Move the button for cool animations
if (currentMenu == &EXTRAS_MainDef)
{
M_ExtrasQuit(); // reset the options button.
}
else
{
extrasmenu.textx = 160;
extrasmenu.texty = 50;
}
}
boolean M_ExtrasInputs(INT32 ch)
{
const UINT8 pid = 0;
(void) ch;
if (menucmd[pid].dpad_ud > 0)
{
extrasmenu.offset += 48;
M_NextOpt();
S_StartSound(NULL, sfx_s3k5b);
if (itemOn == 0)
extrasmenu.offset -= currentMenu->numitems*48;
M_SetMenuDelay(pid);
return true;
}
else if (menucmd[pid].dpad_ud < 0)
{
extrasmenu.offset -= 48;
M_PrevOpt();
S_StartSound(NULL, sfx_s3k5b);
if (itemOn == currentMenu->numitems-1)
extrasmenu.offset += currentMenu->numitems*48;
M_SetMenuDelay(pid);
return true;
}
else if (M_MenuConfirmPressed(pid))
{
if (currentMenu->menuitems[itemOn].status & IT_TRANSTEXT)
return true; // No.
extrasmenu.extx = 140;
extrasmenu.exty = 70; // Default position for the currently selected option.
M_SetMenuDelay(pid);
return false; // Don't eat.
}
return false;
}

359
src/menus/extras-addons.c Normal file
View file

@ -0,0 +1,359 @@
/// \file menus/extras-addons.c
/// \brief Addons menu!
#include "../k_menu.h"
#include "../filesrch.h" // Addfile
#include "../d_main.h"
#include "../z_zone.h"
#include "../s_sound.h"
#include "../v_video.h"
menuitem_t MISC_AddonsMenu[] =
{
{IT_STRING | IT_CVAR | IT_CV_STRING, NULL, NULL,
NULL, {.cvar = &cv_dummyaddonsearch}, 0, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, NULL,
NULL, {.routine = M_HandleAddons}, 0, 0}, // dummy menuitem for the control func
};
menu_t MISC_AddonsDef = {
sizeof (MISC_AddonsMenu)/sizeof (menuitem_t),
NULL,
0,
MISC_AddonsMenu,
50, 28,
0, 0,
0, 0,
M_DrawAddons,
M_AddonsRefresh,
NULL,
NULL,
NULL
};
// Addons menu: (Merely copypasted, original code by toaster)
static void M_UpdateAddonsSearch(void);
consvar_t cv_dummyaddonsearch = CVAR_INIT ("dummyaddonsearch", "", CV_HIDDEN|CV_CALL|CV_NOINIT, NULL, M_UpdateAddonsSearch);
void M_Addons(INT32 choice)
{
const char *pathname = ".";
(void)choice;
#if 1
if (cv_addons_option.value == 0)
pathname = addonsdir;
else if (cv_addons_option.value == 1)
pathname = srb2home;
else if (cv_addons_option.value == 2)
pathname = srb2path;
else
#endif
if (cv_addons_option.value == 3 && *cv_addons_folder.string != '\0')
pathname = cv_addons_folder.string;
strlcpy(menupath, pathname, 1024);
menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath) + 1;
if (menupath[menupathindex[menudepthleft]-2] != PATHSEP[0])
{
menupath[menupathindex[menudepthleft]-1] = PATHSEP[0];
menupath[menupathindex[menudepthleft]] = 0;
}
else
--menupathindex[menudepthleft];
if (!preparefilemenu(false, false))
{
M_StartMessage(va("No files/folders found.\n\n%s\n\nPress (B)\n", LOCATIONSTRING1),NULL,MM_NOTHING);
return;
}
else
dir_on[menudepthleft] = 0;
MISC_AddonsDef.lastOn = 0; // Always start on search
MISC_AddonsDef.prevMenu = currentMenu;
M_SetupNextMenu(&MISC_AddonsDef, false);
}
char *M_AddonsHeaderPath(void)
{
UINT32 len;
static char header[1024];
strlcpy(header, va("%s folder%s", cv_addons_option.string, menupath+menupathindex[menudepth-1]-1), 1024);
len = strlen(header);
if (len > 34)
{
len = len-34;
header[len] = header[len+1] = header[len+2] = '.';
}
else
len = 0;
return header+len;
}
#define UNEXIST S_StartSound(NULL, sfx_s26d);\
M_SetupNextMenu(MISC_AddonsDef.prevMenu, false);\
M_StartMessage(va("\x82%s\x80\nThis folder no longer exists!\nAborting to main menu.\n\nPress (B)\n", M_AddonsHeaderPath()),NULL,MM_NOTHING)
#define CLEARNAME Z_Free(refreshdirname);\
refreshdirname = NULL
static boolean prevmajormods = false;
static void M_AddonsClearName(INT32 choice)
{
if (!majormods || prevmajormods)
{
CLEARNAME;
}
M_StopMessage(choice);
}
// Handles messages for addon errors.
void M_AddonsRefresh(void)
{
if ((refreshdirmenu & REFRESHDIR_NORMAL) && !preparefilemenu(true, false))
{
UNEXIST;
if (refreshdirname)
{
CLEARNAME;
}
return;// true;
}
#ifdef DEVELOP
prevmajormods = majormods;
#else
if (!majormods && prevmajormods)
prevmajormods = false;
#endif
if ((refreshdirmenu & REFRESHDIR_ADDFILE) || (majormods && !prevmajormods))
{
char *message = NULL;
if (refreshdirmenu & REFRESHDIR_NOTLOADED)
{
S_StartSound(NULL, sfx_s26d);
if (refreshdirmenu & REFRESHDIR_MAX)
message = va("%c%s\x80\nMaximum number of addons reached.\nA file could not be loaded.\nIf you wish to play with this addon, restart the game to clear existing ones.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
else
message = va("%c%s\x80\nA file was not loaded.\nCheck the console log for more info.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
}
else if (refreshdirmenu & (REFRESHDIR_WARNING|REFRESHDIR_ERROR))
{
S_StartSound(NULL, sfx_s224);
message = va("%c%s\x80\nA file was loaded with %s.\nCheck the console log for more info.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname, ((refreshdirmenu & REFRESHDIR_ERROR) ? "errors" : "warnings"));
}
else if (majormods && !prevmajormods)
{
S_StartSound(NULL, sfx_s221);
message = va("%c%s\x80\nYou've loaded a gameplay-modifying addon.\n\nRecord Attack has been disabled, but you\ncan still play alone in local Multiplayer.\n\nIf you wish to play Record Attack mode, restart the game to disable loaded addons.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), refreshdirname);
prevmajormods = majormods;
}
if (message)
{
M_StartMessage(message,FUNCPTRCAST(M_AddonsClearName),MM_YESNO);
return;// true;
}
S_StartSound(NULL, sfx_s221);
CLEARNAME;
}
return;// false;
}
static void M_AddonExec(INT32 ch)
{
if (ch == MA_YES)
{
S_StartSound(NULL, sfx_zoom);
COM_BufAddText(va("exec \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING));
}
}
static void M_UpdateAddonsSearch(void)
{
menusearch[0] = strlen(cv_dummyaddonsearch.string);
strlcpy(menusearch+1, cv_dummyaddonsearch.string, MAXSTRINGLENGTH);
if (!cv_addons_search_case.value)
strupr(menusearch+1);
#if 0 // much slower
if (!preparefilemenu(true, false))
{
UNEXIST;
return;
}
#else // streamlined
searchfilemenu(NULL);
#endif
}
void M_HandleAddons(INT32 choice)
{
const UINT8 pid = 0;
boolean exitmenu = false; // exit to previous menu
(void) choice;
if (menucmd[pid].dpad_ud > 0)
{
if (dir_on[menudepthleft] < sizedirmenu-1)
{
dir_on[menudepthleft]++;
S_StartSound(NULL, sfx_s3k5b);
}
else if (M_NextOpt())
{
S_StartSound(NULL, sfx_s3k5b);
}
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
if (dir_on[menudepthleft])
{
dir_on[menudepthleft]--;
S_StartSound(NULL, sfx_s3k5b);
}
else if (M_PrevOpt())
{
S_StartSound(NULL, sfx_s3k5b);
}
M_SetMenuDelay(pid);
}
else if (M_MenuButtonPressed(pid, MBT_L))
{
UINT8 i;
for (i = numaddonsshown; i && (dir_on[menudepthleft] < sizedirmenu-1); i--)
dir_on[menudepthleft]++;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (M_MenuButtonPressed(pid, MBT_R))
{
UINT8 i;
for (i = numaddonsshown; i && (dir_on[menudepthleft]); i--)
dir_on[menudepthleft]--;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (M_MenuConfirmPressed(pid))
{
boolean refresh = true;
M_SetMenuDelay(pid);
if (!dirmenu[dir_on[menudepthleft]])
S_StartSound(NULL, sfx_s26d);
else
{
switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE])
{
case EXT_FOLDER:
strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING);
if (menudepthleft)
{
menupathindex[--menudepthleft] = strlen(menupath);
menupath[menupathindex[menudepthleft]] = 0;
if (!preparefilemenu(false, false))
{
S_StartSound(NULL, sfx_s224);
M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
menupath[menupathindex[++menudepthleft]] = 0;
if (!preparefilemenu(true, false))
{
UNEXIST;
return;
}
}
else
{
S_StartSound(NULL, sfx_s3k5b);
dir_on[menudepthleft] = 1;
}
refresh = false;
}
else
{
S_StartSound(NULL, sfx_s26d);
M_StartMessage(va("%c%s\x80\nThis folder is too deep to navigate to!\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
menupath[menupathindex[menudepthleft]] = 0;
}
break;
case EXT_UP:
S_StartSound(NULL, sfx_s3k5b);
menupath[menupathindex[++menudepthleft]] = 0;
if (!preparefilemenu(false, false))
{
UNEXIST;
return;
}
break;
case EXT_TXT:
M_StartMessage(va("%c%s\x80\nThis file may not be a console script.\nAttempt to run anyways? \n\nPress (A) to confirm or (B) to cancel\n\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), dirmenu[dir_on[menudepthleft]]+DIR_STRING),FUNCPTRCAST(M_AddonExec),MM_YESNO);
break;
case EXT_CFG:
M_StartMessage(va("%c%s\x80\nThis file may modify your settings.\nAttempt to run anyways? \n\nPress (A) to confirm or (B) to cancel\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), dirmenu[dir_on[menudepthleft]]+DIR_STRING),FUNCPTRCAST(M_AddonExec),MM_YESNO);
break;
case EXT_LUA:
case EXT_SOC:
case EXT_WAD:
#ifdef USE_KART
case EXT_KART:
#endif
case EXT_PK3:
COM_BufAddText(va("addfile \"%s%s\"", menupath, dirmenu[dir_on[menudepthleft]]+DIR_STRING));
break;
default:
S_StartSound(NULL, sfx_s26d);
}
if (refresh)
refreshdirmenu |= REFRESHDIR_NORMAL;
}
}
else if (M_MenuBackPressed(pid))
{
exitmenu = true;
M_SetMenuDelay(pid);
}
if (exitmenu)
{
closefilemenu(true);
// Secret menu!
//MainMenu[secrets].status = (M_AnySecretUnlocked()) ? (IT_STRING | IT_CALL) : (IT_DISABLED);
if (currentMenu->prevMenu)
M_SetupNextMenu(currentMenu->prevMenu, false);
else
M_ClearMenus(true);
M_SetMenuDelay(pid);
}
}

View file

@ -0,0 +1,601 @@
/// \file menus/extras-challenges.c
/// \brief Challenges.
#include "../k_menu.h"
#include "../m_cond.h" // Condition Sets
#include "../m_random.h" // And just some randomness for the exits.
#include "../z_zone.h"
#include "../r_skins.h"
#include "../s_sound.h"
menuitem_t MISC_ChallengesStatsDummyMenu[] =
{
{IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0},
};
menu_t MISC_ChallengesDef = {
sizeof (MISC_ChallengesStatsDummyMenu)/sizeof (menuitem_t),
&MainDef,
0,
MISC_ChallengesStatsDummyMenu,
BASEVIDWIDTH/2, 32,
0, 0,
98, 0,
M_DrawChallenges,
M_ChallengesTick,
NULL,
NULL,
M_ChallengesInputs,
};
// This must be defined here so it can take sizeof
// MISC_ChallengesStatsDummyMenu :V
menu_t MISC_StatisticsDef = {
sizeof (MISC_ChallengesStatsDummyMenu)/sizeof (menuitem_t),
&MainDef,
0,
MISC_ChallengesStatsDummyMenu,
280, 185,
0, 0,
98, 0,
M_DrawStatistics,
NULL,
NULL,
NULL,
M_StatisticsInputs,
};
struct challengesmenu_s challengesmenu;
menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
{
UINT8 i;
M_UpdateUnlockablesAndExtraEmblems(false);
if ((challengesmenu.pending = challengesmenu.requestnew = (M_GetNextAchievedUnlock() < MAXUNLOCKABLES)))
{
MISC_ChallengesDef.prevMenu = desiredmenu;
}
if (challengesmenu.pending || desiredmenu == NULL)
{
challengesmenu.currentunlock = MAXUNLOCKABLES;
challengesmenu.unlockcondition = NULL;
M_PopulateChallengeGrid();
if (gamedata->challengegrid)
challengesmenu.extradata = M_ChallengeGridExtraData();
memset(setup_explosions, 0, sizeof(setup_explosions));
memset(&challengesmenu.unlockcount, 0, sizeof(challengesmenu.unlockcount));
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
challengesmenu.unlockcount[CC_TOTAL]++;
if (!gamedata->unlocked[i])
{
continue;
}
challengesmenu.unlockcount[CC_UNLOCKED]++;
}
return &MISC_ChallengesDef;
}
return desiredmenu;
}
static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh)
{
UINT8 i;
SINT8 work;
if (unlockid >= MAXUNLOCKABLES)
return;
challengesmenu.currentunlock = unlockid;
challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock);
challengesmenu.unlockanim = 0;
if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL)
return;
for (i = 0; i < (CHALLENGEGRIDHEIGHT * gamedata->challengegridwidth); i++)
{
if (gamedata->challengegrid[i] != unlockid)
{
// Not what we're looking for.
continue;
}
if (challengesmenu.extradata[i] & CHE_CONNECTEDLEFT)
{
// no need to check for CHE_CONNECTEDUP in linear iteration
continue;
}
// Helper calculation for non-fresh scrolling.
work = (challengesmenu.col + challengesmenu.focusx);
challengesmenu.col = challengesmenu.hilix = i/CHALLENGEGRIDHEIGHT;
challengesmenu.row = challengesmenu.hiliy = i%CHALLENGEGRIDHEIGHT;
if (fresh)
{
// We're just entering the menu. Immediately jump to the desired position...
challengesmenu.focusx = 0;
// ...and since the menu is even-width, randomly select whether it's left or right of center.
if (!unlockables[unlockid].majorunlock
&& M_RandomChance(FRACUNIT/2))
challengesmenu.focusx--;
}
else
{
// We're jumping between multiple unlocks in sequence. Get the difference (looped from -range/2 < work <= range/2).
work -= challengesmenu.col;
if (work <= -gamedata->challengegridwidth/2)
work += gamedata->challengegridwidth;
else if (work >= gamedata->challengegridwidth/2)
work -= gamedata->challengegridwidth;
if (work > 0)
{
// We only need to scroll as far as the rightward edge.
if (unlockables[unlockid].majorunlock)
{
work--;
challengesmenu.col++;
if (challengesmenu.col >= gamedata->challengegridwidth)
challengesmenu.col = 0;
}
// Offset right, scroll left?
if (work > LEFTUNLOCKSCROLL)
{
work -= LEFTUNLOCKSCROLL;
challengesmenu.focusx = LEFTUNLOCKSCROLL;
}
else
{
challengesmenu.focusx = work;
work = 0;
}
}
else if (work < 0)
{
// Offset left, scroll right?
if (work < -RIGHTUNLOCKSCROLL)
{
challengesmenu.focusx = -RIGHTUNLOCKSCROLL;
work += RIGHTUNLOCKSCROLL;
}
else
{
challengesmenu.focusx = work;
work = 0;
}
}
else
{
// We're right where we want to be.
challengesmenu.focusx = 0;
}
// And put the pixel-based scrolling in play, too.
challengesmenu.offset = -work*16;
}
break;
}
}
void M_Challenges(INT32 choice)
{
UINT8 i;
(void)choice;
M_InterruptMenuWithChallenges(NULL);
MISC_ChallengesDef.prevMenu = currentMenu;
if (gamedata->challengegrid != NULL && !challengesmenu.pending)
{
UINT8 selection[MAXUNLOCKABLES];
UINT8 numunlocks = 0;
// Get a random available unlockable.
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
if (!gamedata->unlocked[i])
{
continue;
}
selection[numunlocks++] = i;
}
if (!numunlocks)
{
// ...OK, get a random unlockable.
for (i = 0; i < MAXUNLOCKABLES; i++)
{
if (!unlockables[i].conditionset)
{
continue;
}
selection[numunlocks++] = i;
}
}
M_ChallengesAutoFocus(selection[M_RandomKey(numunlocks)], true);
}
M_SetupNextMenu(&MISC_ChallengesDef, false);
}
void M_ChallengesTick(void)
{
const UINT8 pid = 0;
UINT8 i, newunlock = MAXUNLOCKABLES;
boolean fresh = (challengesmenu.currentunlock >= MAXUNLOCKABLES);
// Ticking
challengesmenu.ticker++;
challengesmenu.offset /= 2;
for (i = 0; i < CSEXPLOSIONS; i++)
{
if (setup_explosions[i].tics > 0)
setup_explosions[i].tics--;
}
if (challengesmenu.unlockcount[CC_ANIM] > 0)
challengesmenu.unlockcount[CC_ANIM]--;
M_CupSelectTick();
if (challengesmenu.pending)
{
// Pending mode.
if (challengesmenu.requestnew)
{
// The menu apparatus is requesting a new unlock.
challengesmenu.requestnew = false;
if ((newunlock = M_GetNextAchievedUnlock()) < MAXUNLOCKABLES)
{
// We got one!
M_ChallengesAutoFocus(newunlock, fresh);
}
else
{
// All done! Let's save the unlocks we've busted open.
challengesmenu.pending = false;
G_SaveGameData();
}
}
else if (challengesmenu.fade < 5)
{
// Fade increase.
challengesmenu.fade++;
}
else
{
// Unlock sequence.
tic_t nexttime = M_MenuExtraHeld(pid) ? (UNLOCKTIME*2) : MAXUNLOCKTIME;
if (++challengesmenu.unlockanim >= nexttime)
{
challengesmenu.requestnew = true;
}
if (challengesmenu.currentunlock < MAXUNLOCKABLES
&& challengesmenu.unlockanim == UNLOCKTIME)
{
// Unlock animation... also tied directly to the actual unlock!
gamedata->unlocked[challengesmenu.currentunlock] = true;
M_UpdateUnlockablesAndExtraEmblems(true);
// Update shown description just in case..?
challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock);
challengesmenu.unlockcount[CC_TALLY]++;
challengesmenu.unlockcount[CC_ANIM]++;
Z_Free(challengesmenu.extradata);
if ((challengesmenu.extradata = M_ChallengeGridExtraData()))
{
unlockable_t *ref = &unlockables[challengesmenu.currentunlock];
UINT16 bombcolor = SKINCOLOR_NONE;
if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors)
{
bombcolor = ref->color;
}
else switch (ref->type)
{
case SECRET_SKIN:
{
INT32 skin = M_UnlockableSkinNum(ref);
if (skin != -1)
{
bombcolor = skins[skin].prefcolor;
}
break;
}
case SECRET_FOLLOWER:
{
INT32 skin = M_UnlockableFollowerNum(ref);
if (skin != -1)
{
bombcolor = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value);
}
break;
}
default:
break;
}
if (bombcolor == SKINCOLOR_NONE)
{
bombcolor = cv_playercolor[0].value;
}
i = (ref->majorunlock && M_RandomChance(FRACUNIT/2)) ? 1 : 0;
M_SetupReadyExplosions(false, challengesmenu.hilix, challengesmenu.hiliy+i, bombcolor);
if (ref->majorunlock)
{
M_SetupReadyExplosions(false, challengesmenu.hilix+1, challengesmenu.hiliy+(1-i), bombcolor);
}
S_StartSound(NULL, sfx_s3k4e);
}
}
}
}
else
{
// Tick down the tally. (currently not visible)
/*if ((challengesmenu.ticker & 1)
&& challengesmenu.unlockcount[CC_TALLY] > 0)
{
challengesmenu.unlockcount[CC_TALLY]--;
challengesmenu.unlockcount[CC_UNLOCKED]++;
}*/
if (challengesmenu.fade > 0)
{
// Fade decrease.
challengesmenu.fade--;
}
}
}
boolean M_ChallengesInputs(INT32 ch)
{
const UINT8 pid = 0;
UINT8 i;
const boolean start = M_MenuButtonPressed(pid, MBT_START);
const boolean move = (menucmd[pid].dpad_ud != 0 || menucmd[pid].dpad_lr != 0);
(void) ch;
if (challengesmenu.fade)
{
;
}
#ifdef DEVELOP
else if (M_MenuExtraPressed(pid) && challengesmenu.extradata) // debugging
{
if (challengesmenu.currentunlock < MAXUNLOCKABLES)
{
Z_Free(gamedata->challengegrid);
gamedata->challengegrid = NULL;
gamedata->challengegridwidth = 0;
M_PopulateChallengeGrid();
Z_Free(challengesmenu.extradata);
challengesmenu.extradata = M_ChallengeGridExtraData();
M_ChallengesAutoFocus(challengesmenu.currentunlock, true);
challengesmenu.pending = true;
}
return true;
}
#endif
else
{
if (M_MenuBackPressed(pid) || start)
{
M_GoBack(0);
M_SetMenuDelay(pid);
Z_Free(challengesmenu.extradata);
challengesmenu.extradata = NULL;
challengesmenu.unlockcondition = NULL;
return true;
}
if (challengesmenu.extradata != NULL && move)
{
// Determine movement around the grid
// For right/down movement, we can pre-determine the number of steps based on extradata.
// For left/up movement, we can't - we have to be ready to iterate twice, and break early if we don't run into a large tile.
if (menucmd[pid].dpad_ud > 0)
{
i = 2;
while (i > 0)
{
if (challengesmenu.row < CHALLENGEGRIDHEIGHT-1)
{
challengesmenu.row++;
}
else
{
challengesmenu.row = 0;
}
if (!(challengesmenu.extradata[
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
+ challengesmenu.row]
& CHE_CONNECTEDUP))
{
break;
}
i--;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
i = (challengesmenu.extradata[
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
+ challengesmenu.row]
& CHE_CONNECTEDUP) ? 2 : 1;
while (i > 0)
{
if (challengesmenu.row > 0)
{
challengesmenu.row--;
}
else
{
challengesmenu.row = CHALLENGEGRIDHEIGHT-1;
}
i--;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
if (menucmd[pid].dpad_lr > 0)
{
i = 2;
while (i > 0)
{
// Slide the focus counter to movement, if we can.
if (challengesmenu.focusx > -RIGHTUNLOCKSCROLL)
{
challengesmenu.focusx--;
}
// Step the actual column right.
if (challengesmenu.col < gamedata->challengegridwidth-1)
{
challengesmenu.col++;
}
else
{
challengesmenu.col = 0;
}
if (!(challengesmenu.extradata[
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
+ challengesmenu.row]
& CHE_CONNECTEDLEFT))
{
break;
}
i--;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr < 0)
{
i = (challengesmenu.extradata[
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
+ challengesmenu.row]
& CHE_CONNECTEDLEFT) ? 2 : 1;
while (i > 0)
{
// Slide the focus counter to movement, if we can.
if (challengesmenu.focusx < LEFTUNLOCKSCROLL)
{
challengesmenu.focusx++;
}
// Step the actual column left.
if (challengesmenu.col > 0)
{
challengesmenu.col--;
}
else
{
challengesmenu.col = gamedata->challengegridwidth-1;
}
i--;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
// After movement has been determined, figure out the current selection.
i = (challengesmenu.col * CHALLENGEGRIDHEIGHT) + challengesmenu.row;
challengesmenu.currentunlock = (gamedata->challengegrid[i]);
challengesmenu.unlockcondition = M_BuildConditionSetString(challengesmenu.currentunlock);
challengesmenu.hilix = challengesmenu.col;
challengesmenu.hiliy = challengesmenu.row;
if (challengesmenu.currentunlock < MAXUNLOCKABLES
&& unlockables[challengesmenu.currentunlock].majorunlock)
{
// Adjust highlight coordinates up/to the left for large tiles.
if (challengesmenu.hiliy > 0 && (challengesmenu.extradata[i] & CHE_CONNECTEDUP))
{
challengesmenu.hiliy--;
}
if ((challengesmenu.extradata[i] & CHE_CONNECTEDLEFT))
{
if (challengesmenu.hilix > 0)
{
challengesmenu.hilix--;
}
else
{
challengesmenu.hilix = gamedata->challengegridwidth-1;
}
}
}
return true;
}
if (M_MenuConfirmPressed(pid)
&& challengesmenu.currentunlock < MAXUNLOCKABLES
&& gamedata->unlocked[challengesmenu.currentunlock])
{
switch (unlockables[challengesmenu.currentunlock].type)
{
case SECRET_ALTTITLE:
CV_AddValue(&cv_alttitle, 1);
S_StartSound(NULL, sfx_s3kc3s);
M_SetMenuDelay(pid);
break;
default:
break;
}
return true;
}
}
return true;
}

View file

@ -0,0 +1,292 @@
/// \file menus/extras-replay-hut.c
/// \brief Extras Menu: Replay Hut
#include "../k_menu.h"
#include "../filesrch.h" // Addfile
#include "../d_main.h"
#include "../s_sound.h"
#include "../v_video.h"
#include "../z_zone.h"
// extras menu: replay hut
menuitem_t EXTRAS_ReplayHut[] =
{
{IT_KEYHANDLER|IT_NOTHING, "", "", // Dummy menuitem for the replay list
NULL, {.routine = M_HandleReplayHutList}, 0, 0},
{IT_NOTHING, "", "", // Dummy for handling wrapping to the top of the menu..
NULL, {NULL}, 0, 0},
};
menu_t EXTRAS_ReplayHutDef =
{
sizeof (EXTRAS_ReplayHut)/sizeof (menuitem_t),
&EXTRAS_MainDef,
0,
EXTRAS_ReplayHut,
30, 80,
0, 0,
0, 0,
M_DrawReplayHut,
NULL,
NULL,
M_QuitReplayHut,
NULL
};
menuitem_t EXTRAS_ReplayStart[] =
{
{IT_CALL |IT_STRING, "Load Addons and Watch", NULL,
NULL, {.routine = M_HutStartReplay}, 0, 0},
{IT_CALL |IT_STRING, "Load Without Addons", NULL,
NULL, {.routine = M_HutStartReplay}, 10, 0},
{IT_CALL |IT_STRING, "Watch Replay", NULL,
NULL, {.routine = M_HutStartReplay}, 10, 0},
{IT_SUBMENU |IT_STRING, "Go Back", NULL,
NULL, {.submenu = &EXTRAS_ReplayHutDef}, 30, 0},
};
menu_t EXTRAS_ReplayStartDef =
{
sizeof (EXTRAS_ReplayStart)/sizeof (menuitem_t),
&EXTRAS_ReplayHutDef,
0,
EXTRAS_ReplayStart,
27, 80,
0, 0,
0, 0,
M_DrawReplayStartMenu,
NULL,
NULL,
NULL,
NULL
};
void M_PrepReplayList(void)
{
size_t i;
if (extrasmenu.demolist)
Z_Free(extrasmenu.demolist);
extrasmenu.demolist = Z_Calloc(sizeof(menudemo_t) * sizedirmenu, PU_STATIC, NULL);
for (i = 0; i < sizedirmenu; i++)
{
if (dirmenu[i][DIR_TYPE] == EXT_UP)
{
extrasmenu.demolist[i].type = MD_SUBDIR;
sprintf(extrasmenu.demolist[i].title, "UP");
}
else if (dirmenu[i][DIR_TYPE] == EXT_FOLDER)
{
extrasmenu.demolist[i].type = MD_SUBDIR;
strncpy(extrasmenu.demolist[i].title, dirmenu[i] + DIR_STRING, 64);
}
else
{
extrasmenu.demolist[i].type = MD_NOTLOADED;
snprintf(extrasmenu.demolist[i].filepath, sizeof extrasmenu.demolist[i].filepath,
// 255 = UINT8 limit. dirmenu entries are restricted to this length (see DIR_LEN).
"%s%.255s", menupath, dirmenu[i] + DIR_STRING);
sprintf(extrasmenu.demolist[i].title, ".....");
}
}
}
void M_ReplayHut(INT32 choice)
{
(void)choice;
extrasmenu.replayScrollTitle = 0;
extrasmenu.replayScrollDelay = TICRATE;
extrasmenu.replayScrollDir = 1;
if (!demo.inreplayhut)
{
snprintf(menupath, 1024, "%s"PATHSEP"media"PATHSEP"replay"PATHSEP"online"PATHSEP, srb2home);
menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath);
}
if (!preparefilemenu(false, true))
{
M_StartMessage("No replays found.\n\nPress (B)\n", NULL, MM_NOTHING);
return;
}
else if (!demo.inreplayhut)
dir_on[menudepthleft] = 0;
demo.inreplayhut = true;
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
M_PrepReplayList();
menuactive = true;
M_SetupNextMenu(&EXTRAS_ReplayHutDef, false);
//G_SetGamestate(GS_TIMEATTACK);
//titlemapinaction = TITLEMAP_OFF; // Nope don't give us HOMs please
demo.rewinding = false;
CL_ClearRewinds();
//S_ChangeMusicInternal("replst", true);
}
// key handler
void M_HandleReplayHutList(INT32 choice)
{
const UINT8 pid = 0;
(void) choice;
if (menucmd[pid].dpad_ud < 0)
{
if (dir_on[menudepthleft])
dir_on[menudepthleft]--;
else
return;
//M_PrevOpt();
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
}
else if (menucmd[pid].dpad_ud > 0)
{
if (dir_on[menudepthleft] < sizedirmenu-1)
dir_on[menudepthleft]++;
else
return;
//itemOn = 0; // Not M_NextOpt because that would take us to the extra dummy item
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
}
else if (M_MenuBackPressed(pid))
{
M_SetMenuDelay(pid);
M_QuitReplayHut();
}
else if (M_MenuConfirmPressed(pid))
{
M_SetMenuDelay(pid);
switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE])
{
case EXT_FOLDER:
strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING);
if (menudepthleft)
{
menupathindex[--menudepthleft] = strlen(menupath);
menupath[menupathindex[menudepthleft]] = 0;
if (!preparefilemenu(false, true))
{
S_StartSound(NULL, sfx_s224);
M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
menupath[menupathindex[++menudepthleft]] = 0;
if (!preparefilemenu(true, true))
{
M_QuitReplayHut();
return;
}
}
else
{
S_StartSound(NULL, sfx_s3k5b);
dir_on[menudepthleft] = 1;
M_PrepReplayList();
}
}
else
{
S_StartSound(NULL, sfx_s26d);
M_StartMessage(va("%c%s\x80\nThis folder is too deep to navigate to!\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
menupath[menupathindex[menudepthleft]] = 0;
}
break;
case EXT_UP:
S_StartSound(NULL, sfx_s3k5b);
menupath[menupathindex[++menudepthleft]] = 0;
if (!preparefilemenu(false, true))
{
M_QuitReplayHut();
return;
}
M_PrepReplayList();
break;
default:
// We can't just use M_SetupNextMenu because that'll run ReplayDef's quitroutine and boot us back to the title screen!
currentMenu->lastOn = itemOn;
currentMenu = &EXTRAS_ReplayStartDef;
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
switch (extrasmenu.demolist[dir_on[menudepthleft]].addonstatus)
{
case DFILE_ERROR_CANNOTLOAD:
// Only show "Watch Replay Without Addons"
EXTRAS_ReplayStart[0].status = IT_DISABLED;
EXTRAS_ReplayStart[1].status = IT_CALL|IT_STRING;
//EXTRAS_ReplayStart[1].alphaKey = 0;
EXTRAS_ReplayStart[2].status = IT_DISABLED;
itemOn = 1;
break;
case DFILE_ERROR_NOTLOADED:
case DFILE_ERROR_INCOMPLETEOUTOFORDER:
// Show "Load Addons and Watch Replay" and "Watch Replay Without Addons"
EXTRAS_ReplayStart[0].status = IT_CALL|IT_STRING;
EXTRAS_ReplayStart[1].status = IT_CALL|IT_STRING;
//EXTRAS_ReplayStart[1].alphaKey = 10;
EXTRAS_ReplayStart[2].status = IT_DISABLED;
itemOn = 0;
break;
case DFILE_ERROR_EXTRAFILES:
case DFILE_ERROR_OUTOFORDER:
default:
// Show "Watch Replay"
EXTRAS_ReplayStart[0].status = IT_DISABLED;
EXTRAS_ReplayStart[1].status = IT_DISABLED;
EXTRAS_ReplayStart[2].status = IT_CALL|IT_STRING;
//EXTRAS_ReplayStart[2].alphaKey = 0;
itemOn = 2;
break;
}
}
}
}
boolean M_QuitReplayHut(void)
{
// D_StartTitle does its own wipe, since GS_TIMEATTACK is now a complete gamestate.
menuactive = false;
D_StartTitle();
if (extrasmenu.demolist)
Z_Free(extrasmenu.demolist);
extrasmenu.demolist = NULL;
demo.inreplayhut = false;
return true;
}
void M_HutStartReplay(INT32 choice)
{
(void)choice;
M_ClearMenus(false);
demo.loadfiles = (itemOn == 0);
demo.ignorefiles = (itemOn != 0);
G_DoPlayDemo(extrasmenu.demolist[dir_on[menudepthleft]].filepath);
}

View file

@ -0,0 +1,99 @@
/// \file menus/extras-challenges.c
/// \brief Statistics menu
#include "../k_menu.h"
#include "../z_zone.h"
#include "../m_cond.h" // Condition Sets
#include "../s_sound.h"
struct statisticsmenu_s statisticsmenu;
void M_Statistics(INT32 choice)
{
UINT16 i = 0;
(void)choice;
statisticsmenu.maplist = Z_Malloc(sizeof(UINT16) * nummapheaders, PU_STATIC, NULL);
statisticsmenu.nummaps = 0;
for (i = 0; i < nummapheaders; i++)
{
if (!mapheaderinfo[i])
continue;
// Check for no visibility + legacy box
if (mapheaderinfo[i]->menuflags & (LF2_NOTIMEATTACK|LF2_HIDEINSTATS|LF2_HIDEINMENU))
continue;
// Check for completion
if ((mapheaderinfo[i]->menuflags & LF2_FINISHNEEDED)
&& !(mapheaderinfo[i]->mapvisited & MV_BEATEN))
continue;
// Check for unlock
if (M_MapLocked(i+1))
continue;
statisticsmenu.maplist[statisticsmenu.nummaps++] = i;
}
statisticsmenu.maplist[statisticsmenu.nummaps] = NEXTMAP_INVALID;
statisticsmenu.maxscroll = (statisticsmenu.nummaps + M_CountMedals(true, true) + 2) - 10;
statisticsmenu.location = 0;
if (statisticsmenu.maxscroll < 0)
{
statisticsmenu.maxscroll = 0;
}
MISC_StatisticsDef.prevMenu = currentMenu;
M_SetupNextMenu(&MISC_StatisticsDef, false);
}
boolean M_StatisticsInputs(INT32 ch)
{
const UINT8 pid = 0;
(void)ch;
if (M_MenuBackPressed(pid))
{
M_GoBack(0);
M_SetMenuDelay(pid);
Z_Free(statisticsmenu.maplist);
statisticsmenu.maplist = NULL;
return true;
}
if (M_MenuExtraPressed(pid))
{
if (statisticsmenu.location > 0)
{
statisticsmenu.location = 0;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
}
else if (menucmd[pid].dpad_ud > 0)
{
if (statisticsmenu.location < statisticsmenu.maxscroll)
{
statisticsmenu.location++;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
}
else if (menucmd[pid].dpad_ud < 0)
{
if (statisticsmenu.location > 0)
{
statisticsmenu.location--;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
}
return true;
}

112
src/menus/main-1.c Normal file
View file

@ -0,0 +1,112 @@
/// \file menus/main-1.c
/// \brief Main Menu
// ==========================================================================
// ORGANIZATION START.
// ==========================================================================
// Note: Never should we be jumping from one category of menu options to another
// without first going to the Main Menu.
// Note: Ignore the above if you're working with the Pause menu.
// Note: (Prefix)_MainMenu should be the target of all Main Menu options that
// point to submenus.
#include "../k_menu.h"
#include "../m_random.h"
#include "../s_sound.h"
#include "../i_time.h"
#include "../v_video.h"
#include "../z_zone.h"
#include "../i_video.h" // I_FinishUpdate
#include "../i_system.h" // I_Sleep
menuitem_t MainMenu[] =
{
{IT_STRING | IT_CALL, "Play",
"Cut to the chase and start the race!", NULL,
{.routine = M_CharacterSelect}, 0, 0},
{IT_STRING | IT_CALL, "Extras",
"Check out some bonus features.", "MENUI001",
{.routine = M_InitExtras}, 0, 0},
{IT_STRING, "Options",
"Configure your controls, settings, and preferences.", NULL,
{.routine = M_InitOptions}, 0, 0},
{IT_STRING | IT_CALL, "Quit",
"Exit \"Dr. Robotnik's Ring Racers\".", NULL,
{.routine = M_QuitSRB2}, 0, 0},
};
menu_t MainDef = KARTGAMEMODEMENU(MainMenu, NULL);
// Quit Game
static INT32 quitsounds[] =
{
// holy shit we're changing things up!
// srb2kart: you ain't seen nothing yet
sfx_kc2e,
sfx_kc2f,
sfx_cdfm01,
sfx_ddash,
sfx_s3ka2,
sfx_s3k49,
sfx_slip,
sfx_tossed,
sfx_s3k7b,
sfx_itrolf,
sfx_itrole,
sfx_cdpcm9,
sfx_s3k4e,
sfx_s259,
sfx_3db06,
sfx_s3k3a,
sfx_peel,
sfx_cdfm28,
sfx_s3k96,
sfx_s3kc0s,
sfx_cdfm39,
sfx_hogbom,
sfx_kc5a,
sfx_kc46,
sfx_s3k92,
sfx_s3k42,
sfx_kpogos,
sfx_screec
};
void M_QuitSRB2(INT32 choice)
{
// We pick index 0 which is language sensitive, or one at random,
// between 1 and maximum number.
(void)choice;
M_StartMessage("Are you sure you want to quit playing?\n\nPress (A) to confirm or (B) to cancel", FUNCPTRCAST(M_QuitResponse), MM_YESNO);
}
void M_QuitResponse(INT32 ch)
{
tic_t ptime;
INT32 mrand;
if (ch == MA_YES)
{
if (!(netgame || cht_debug))
{
mrand = M_RandomKey(sizeof(quitsounds) / sizeof(INT32));
if (quitsounds[mrand])
S_StartSound(NULL, quitsounds[mrand]);
//added : 12-02-98: do that instead of I_WaitVbl which does not work
ptime = I_GetTime() + NEWTICRATE*2; // Shortened the quit time, used to be 2 seconds Tails 03-26-2001
while (ptime > I_GetTime())
{
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
V_DrawSmallScaledPatch(0, 0, 0, W_CachePatchName("GAMEQUIT", PU_CACHE)); // Demo 3 Quit Screen Tails 06-16-2001
I_FinishUpdate(); // Update the screen with the image Tails 06-19-2001
I_Sleep(cv_sleep.value);
I_UpdateTime(cv_timescale.value);
}
}
I_Quit();
}
}

View file

@ -0,0 +1,24 @@
/// \file menus/main-profile-select.c
/// \brief Duplicate for main profile select.
#include "../k_menu.h"
menuitem_t MAIN_Profiles[] = {
{IT_KEYHANDLER | IT_NOTHING, NULL, "Select a profile to use or create a new Profile.",
NULL, {.routine = M_HandleProfileSelect}, 0, 0}, // dummy menuitem for the control func
};
menu_t MAIN_ProfilesDef = {
sizeof (MAIN_Profiles) / sizeof (menuitem_t),
NULL,
0,
MAIN_Profiles,
32, 80,
SKINCOLOR_ULTRAMARINE, 0,
2, 5,
M_DrawProfileSelect,
M_OptionsTick,
NULL,
NULL,
NULL,
};

230
src/menus/options-1.c Normal file
View file

@ -0,0 +1,230 @@
/// \file menus/options-1.c
/// \brief Options Menu
#include "../k_menu.h"
#include "../k_grandprix.h" // K_CanChangeRules
#include "../m_cond.h" // Condition Sets
#include "../s_sound.h"
// options menu -- see mopt_e
menuitem_t OPTIONS_Main[] =
{
{IT_STRING | IT_CALL, "Profile Setup", "Remap keys & buttons to your likings.",
NULL, {.routine = M_ProfileSelectInit}, 0, 0},
{IT_STRING | IT_SUBMENU, "Video Options", "Change video settings such as the resolution.",
NULL, {.submenu = &OPTIONS_VideoDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Sound Options", "Adjust various sound settings such as the volume.",
NULL, {.submenu = &OPTIONS_SoundDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "HUD Options", "Options related to the Heads-Up Display.",
NULL, {.submenu = &OPTIONS_HUDDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Gameplay Options", "Change various game related options",
NULL, {.submenu = &OPTIONS_GameplayDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Server Options", "Change various specific options for your game server.",
NULL, {.submenu = &OPTIONS_ServerDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Data Options", "Miscellaneous data options such as the screenshot format.",
NULL, {.submenu = &OPTIONS_DataDef}, 0, 0},
{IT_STRING | IT_CALL, "Tricks & Secrets", "Those who bother reading a game manual always get the edge over those who don't!",
NULL, {.routine = M_Manual}, 0, 0},
};
// For options menu, the 'extra1' field will determine the background colour to use for... the background! (What a concept!)
menu_t OPTIONS_MainDef = {
sizeof (OPTIONS_Main) / sizeof (menuitem_t),
&MainDef,
0,
OPTIONS_Main,
0, 0,
SKINCOLOR_SLATE, 0,
2, 5,
M_DrawOptions,
M_OptionsTick,
NULL,
NULL,
M_OptionsInputs
};
struct optionsmenu_s optionsmenu;
void M_ResetOptions(void)
{
optionsmenu.ticker = 0;
optionsmenu.offset = 0;
optionsmenu.optx = 0;
optionsmenu.opty = 0;
optionsmenu.toptx = 0;
optionsmenu.topty = 0;
// BG setup:
optionsmenu.currcolour = OPTIONS_MainDef.extra1;
optionsmenu.lastcolour = 0;
optionsmenu.fade = 0;
// For profiles:
memset(setup_player, 0, sizeof(setup_player));
optionsmenu.profile = NULL;
}
void M_InitOptions(INT32 choice)
{
(void)choice;
OPTIONS_MainDef.menuitems[mopt_gameplay].status = IT_STRING | IT_TRANSTEXT;
OPTIONS_MainDef.menuitems[mopt_server].status = IT_STRING | IT_TRANSTEXT;
// enable gameplay & server options under the right circumstances.
if (gamestate == GS_MENU
|| ((server || IsPlayerAdmin(consoleplayer)) && K_CanChangeRules(false)))
{
OPTIONS_MainDef.menuitems[mopt_gameplay].status = IT_STRING | IT_SUBMENU;
OPTIONS_MainDef.menuitems[mopt_server].status = IT_STRING | IT_SUBMENU;
OPTIONS_GameplayDef.menuitems[gopt_encore].status =
(M_SecretUnlocked(SECRET_ENCORE, false) ? (IT_STRING | IT_CVAR) : IT_DISABLED);
}
OPTIONS_DataDef.menuitems[dopt_erase].status = (gamestate == GS_MENU
? (IT_STRING | IT_SUBMENU)
: (IT_TRANSTEXT2 | IT_SPACE));
M_ResetOptions();
// So that pause doesn't go to the main menu...
OPTIONS_MainDef.prevMenu = currentMenu;
// This will disable or enable the textboxes of the affected menus before we get to them.
Screenshot_option_Onchange();
Moviemode_mode_Onchange();
Moviemode_option_Onchange();
Addons_option_Onchange();
M_SetupNextMenu(&OPTIONS_MainDef, false);
}
// Prepares changing the colour of the background
void M_OptionsChangeBGColour(INT16 newcolour)
{
optionsmenu.fade = 10;
optionsmenu.lastcolour = optionsmenu.currcolour;
optionsmenu.currcolour = newcolour;
}
boolean M_OptionsQuit(void)
{
optionsmenu.toptx = 140-1;
optionsmenu.topty = 70+1;
// Reset button behaviour because profile menu is different, since of course it is.
if (optionsmenu.resetprofilemenu)
{
optionsmenu.profilemenu = false;
optionsmenu.profile = NULL;
optionsmenu.resetprofilemenu = false;
}
return true; // Always allow quitting, duh.
}
void M_OptionsTick(void)
{
optionsmenu.offset /= 2;
optionsmenu.ticker++;
optionsmenu.optx += (optionsmenu.toptx - optionsmenu.optx)/2;
optionsmenu.opty += (optionsmenu.topty - optionsmenu.opty)/2;
if (abs(optionsmenu.optx - optionsmenu.opty) < 2)
{
optionsmenu.optx = optionsmenu.toptx;
optionsmenu.opty = optionsmenu.topty; // Avoid awkward 1 px errors.
}
// Move the button for cool animations
if (currentMenu == &OPTIONS_MainDef)
{
M_OptionsQuit(); // ...So now this is used here.
}
else if (optionsmenu.profile == NULL) // Not currently editing a profile (otherwise we're using these variables for other purposes....)
{
// I don't like this, it looks like shit but it needs to be done..........
if (optionsmenu.profilemenu)
{
optionsmenu.toptx = 420;
optionsmenu.topty = 70+1;
}
else if (currentMenu == &OPTIONS_GameplayItemsDef)
{
optionsmenu.toptx = -160; // off the side of the screen
optionsmenu.topty = 50;
}
else
{
optionsmenu.toptx = 160;
optionsmenu.topty = 50;
}
}
// Handle the background stuff:
if (optionsmenu.fade)
optionsmenu.fade--;
// change the colour if we aren't matching the current menu colour
if (optionsmenu.currcolour != currentMenu->extra1)
M_OptionsChangeBGColour(currentMenu->extra1);
// And one last giggle...
if (shitsfree)
shitsfree--;
}
boolean M_OptionsInputs(INT32 ch)
{
const UINT8 pid = 0;
(void)ch;
if (menucmd[pid].dpad_ud > 0)
{
M_SetMenuDelay(pid);
optionsmenu.offset += 48;
M_NextOpt();
S_StartSound(NULL, sfx_s3k5b);
if (itemOn == 0)
optionsmenu.offset -= currentMenu->numitems*48;
return true;
}
else if (menucmd[pid].dpad_ud < 0)
{
M_SetMenuDelay(pid);
optionsmenu.offset -= 48;
M_PrevOpt();
S_StartSound(NULL, sfx_s3k5b);
if (itemOn == currentMenu->numitems-1)
optionsmenu.offset += currentMenu->numitems*48;
return true;
}
else if (M_MenuConfirmPressed(pid))
{
if (currentMenu->menuitems[itemOn].status & IT_TRANSTEXT)
return true; // No.
optionsmenu.optx = 140;
optionsmenu.opty = 70; // Default position for the currently selected option.
return false; // Don't eat.
}
return false;
}

View file

@ -0,0 +1,45 @@
/// \file menus/options-data-1.c
/// \brief Data Options -- see dopt_e
#include "../k_menu.h"
// data options menu -- see dopt_e
menuitem_t OPTIONS_Data[] =
{
{IT_STRING | IT_SUBMENU, "Screenshot Options...", "Set options relative to screenshot and GIF capture.",
NULL, {.submenu = &OPTIONS_DataScreenshotDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Addon Options...", "Set options relative to the addons menu.",
NULL, {.submenu = &OPTIONS_DataAddonDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Replay Options...", "Set options relative to replays.",
NULL, {.submenu = &OPTIONS_DataReplayDef}, 0, 0},
#ifdef HAVE_DISCORDRPC
{IT_STRING | IT_SUBMENU, "Discord Options...", "Set options relative to Discord Rich Presence.",
NULL, {.submenu = &OPTIONS_DataDiscordDef}, 0, 0},
#endif
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "\x85""Erase Data...", "Erase specific data. Be careful, what's deleted is gone forever!",
NULL, {.submenu = &OPTIONS_DataEraseDef}, 0, 0},
};
menu_t OPTIONS_DataDef = {
sizeof (OPTIONS_Data) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Data,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,60 @@
/// \file menus/options-data-addons.c
/// \brief Addon Options
#include "../k_menu.h"
#include "../filesrch.h" // addons cvars
menuitem_t OPTIONS_DataAddon[] =
{
{IT_HEADER, "MENU", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Location", "Where to start searching addons from in the menu.",
NULL, {.cvar = &cv_addons_option}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_STRING, "Custom Folder", "Specify which folder to start searching from if the location is set to custom.",
NULL, {.cvar = &cv_addons_folder}, 24, 0},
{IT_STRING | IT_CVAR, "Identify Addons via", "Set whether to consider the extension or contents of a file.",
NULL, {.cvar = &cv_addons_md5}, 0, 0},
{IT_STRING | IT_CVAR, "Show Unsupported Files", "Sets whether non-addon files should be shown.",
NULL, {.cvar = &cv_addons_showall}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_HEADER, "SEARCH", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Matching", "Set where to check for the text pattern when looking up addons via name.",
NULL, {.cvar = &cv_addons_search_type}, 0, 0},
{IT_STRING | IT_CVAR, "Case Sensitivity", "Set whether to consider the case when searching for addons..",
NULL, {.cvar = &cv_addons_search_case}, 0, 0},
};
menu_t OPTIONS_DataAddonDef = {
sizeof (OPTIONS_DataAddon) / sizeof (menuitem_t),
&OPTIONS_DataDef,
0,
OPTIONS_DataAddon,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};
void Addons_option_Onchange(void)
{
// Option 2 will always be the textbar.
// (keep in mind this is a 0 indexed array and the first element is a header...)
OPTIONS_DataAddon[2].status =
(cv_addons_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
}

View file

@ -0,0 +1,42 @@
/// \file menus/options-data-discord.c
/// \brief Discord Rich Presence Options
#include "../k_menu.h"
#include "../discord.h" // discord rpc cvars
menuitem_t OPTIONS_DataDiscord[] =
{
{IT_STRING | IT_CVAR, "Rich Presence", "Allow Discord to display game info on your status.",
NULL, {.cvar = &cv_discordrp}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_HEADER, "RICH PRESENCE SETTINGS", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Streamer Mode", "Prevents the logging of some account information such as your tag in the console.",
NULL, {.cvar = &cv_discordstreamer}, 0, 0},
{IT_STRING | IT_CVAR, "Allow Ask to Join", "Allow other people to request joining your game from Discord.",
NULL, {.cvar = &cv_discordasks}, 0, 0},
{IT_STRING | IT_CVAR, "Allow Invites", "Set who is allowed to generate Discord invites to your game.",
NULL, {.cvar = &cv_discordinvites}, 0, 0},
};
menu_t OPTIONS_DataDiscordDef = {
sizeof (OPTIONS_DataDiscord) / sizeof (menuitem_t),
&OPTIONS_DataDef,
0,
OPTIONS_DataDiscord,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,84 @@
/// \file menus/options-data-erase-1.c
/// \brief Erase Data Menu
#include "../k_menu.h"
#include "../s_sound.h"
#include "../m_cond.h" // Condition Sets
#include "../f_finale.h"
menuitem_t OPTIONS_DataErase[] =
{
{IT_STRING | IT_CALL, "Erase Time Attack Data", "Be careful! What's deleted is gone forever!",
NULL, {.routine = M_EraseData}, 0, 0},
{IT_STRING | IT_CALL, "Erase Unlockable Data", "Be careful! What's deleted is gone forever!",
NULL, {.routine = M_EraseData}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Erase Profile Data...", "Select a Profile to erase.",
NULL, {.routine = M_CheckProfileData}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "\x85\x45rase all Data", "Be careful! What's deleted is gone forever!",
NULL, {.routine = M_EraseData}, 0, 0},
};
menu_t OPTIONS_DataEraseDef = {
sizeof (OPTIONS_DataErase) / sizeof (menuitem_t),
&OPTIONS_DataDef,
0,
OPTIONS_DataErase,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};
static void M_EraseDataResponse(INT32 ch)
{
if (ch == MA_NO)
return;
S_StartSound(NULL, sfx_itrole); // bweh heh heh
// Delete the data
if (optionsmenu.erasecontext == 2)
{
// SRB2Kart: This actually needs to be done FIRST, so that you don't immediately regain playtime/matches secrets
gamedata->totalplaytime = 0;
gamedata->matchesplayed = 0;
}
if (optionsmenu.erasecontext != 1)
G_ClearRecords();
if (optionsmenu.erasecontext != 0)
M_ClearSecrets();
F_StartIntro();
M_ClearMenus(true);
}
void M_EraseData(INT32 choice)
{
const char *eschoice, *esstr = M_GetText("Are you sure you want to erase\n%s?\n\nPress (A) to confirm or (B) to cancel\n");
optionsmenu.erasecontext = (UINT8)choice;
if (choice == 0)
eschoice = M_GetText("Time Attack data");
else if (choice == 1)
eschoice = M_GetText("Secrets data");
else
eschoice = M_GetText("ALL game data");
M_StartMessage(va(esstr, eschoice), FUNCPTRCAST(M_EraseDataResponse), MM_YESNO);
}

View file

@ -0,0 +1,107 @@
/// \file menus/options-data-erase-profile.c
/// \brief Erase Profile Menu
#include "../k_menu.h"
#include "../s_sound.h"
#include "../f_finale.h"
menuitem_t OPTIONS_DataProfileErase[] =
{
{IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, {.routine = M_HandleProfileErase}, 0, 0},
};
menu_t OPTIONS_DataProfileEraseDef = {
sizeof (OPTIONS_DataProfileErase) / sizeof (menuitem_t),
&OPTIONS_DataEraseDef,
0,
OPTIONS_DataProfileErase,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawProfileErase,
M_OptionsTick,
NULL,
NULL,
NULL
};
// Check if we have any profile loaded.
void M_CheckProfileData(INT32 choice)
{
UINT8 np = PR_GetNumProfiles();
(void) choice;
if (np < 2)
{
S_StartSound(NULL, sfx_s3k7b);
M_StartMessage("There are no custom profiles.\n\nPress (B)", NULL, MM_NOTHING);
return;
}
optionsmenu.eraseprofilen = 1;
M_SetupNextMenu(&OPTIONS_DataProfileEraseDef, false);
}
static void M_EraseProfileResponse(INT32 choice)
{
if (choice == MA_YES)
{
S_StartSound(NULL, sfx_itrole); // bweh heh heh
PR_DeleteProfile(optionsmenu.eraseprofilen);
// Did we bust our current profile..!?
if (cv_currprofile.value == -1)
{
F_StartIntro();
M_ClearMenus(true);
}
else if (optionsmenu.eraseprofilen > PR_GetNumProfiles()-1)
{
optionsmenu.eraseprofilen--;
}
}
}
void M_HandleProfileErase(INT32 choice)
{
const UINT8 pid = 0;
const UINT8 np = PR_GetNumProfiles()-1;
(void) choice;
if (menucmd[pid].dpad_ud > 0)
{
S_StartSound(NULL, sfx_s3k5b);
optionsmenu.eraseprofilen++;
if (optionsmenu.eraseprofilen > np)
optionsmenu.eraseprofilen = 1;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
S_StartSound(NULL, sfx_s3k5b);
if (optionsmenu.eraseprofilen == 1)
optionsmenu.eraseprofilen = np;
else
optionsmenu.eraseprofilen--;
M_SetMenuDelay(pid);
}
else if (M_MenuBackPressed(pid))
{
M_GoBack(0);
M_SetMenuDelay(pid);
}
else if (M_MenuConfirmPressed(pid))
{
if (optionsmenu.eraseprofilen == cv_currprofile.value)
M_StartMessage("Your ""\x85""current profile""\x80"" will be erased.\nAre you sure you want to proceed?\nDeleting this profile will also\nreturn you to the title screen.\n\nPress (A) to confirm or (B) to cancel", FUNCPTRCAST(M_EraseProfileResponse), MM_YESNO);
else
M_StartMessage("This profile will be erased.\nAre you sure you want to proceed?\n\nPress (A) to confirm or (B) to cancel", FUNCPTRCAST(M_EraseProfileResponse), MM_YESNO);
M_SetMenuDelay(pid);
}
}

View file

@ -0,0 +1,28 @@
/// \file menus/options-data-replays.c
/// \brief Replay Options
#include "../k_menu.h"
menuitem_t OPTIONS_DataReplay[] =
{
{IT_STRING | IT_CVAR, "Record Replays", "Select when to save replays.",
NULL, {.cvar = &cv_recordmultiplayerdemos}, 0, 0},
{IT_STRING | IT_CVAR, "Synch. Check Interval", "How often to check for synchronization while playing back a replay.",
NULL, {.cvar = &cv_netdemosyncquality}, 0, 0},
};
menu_t OPTIONS_DataReplayDef = {
sizeof (OPTIONS_DataReplay) / sizeof (menuitem_t),
&OPTIONS_DataDef,
0,
OPTIONS_DataReplay,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,86 @@
/// \file menus/options-data-screenshots.c
/// \brief Screeshot Options
#include "../k_menu.h"
#include "../m_misc.h" // screenshot cvars
menuitem_t OPTIONS_DataScreenshot[] =
{
{IT_HEADER, "SCREENSHOTS (F8)", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Storage Location", "Sets where to store screenshots.",
NULL, {.cvar = &cv_screenshot_option}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_STRING, "Custom Folder", "Specify which folder to save screenshots in.",
NULL, {.cvar = &cv_screenshot_folder}, 24, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_HEADER, "GIF RECORDING (F9)", NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Storage Location", "Sets where to store GIFs",
NULL, {.cvar = &cv_movie_option}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_STRING, "Custom Folder", "Specify which folder to save GIFs in.",
NULL, {.cvar = &cv_movie_folder}, 24, 0},
};
menu_t OPTIONS_DataScreenshotDef = {
sizeof (OPTIONS_DataScreenshot) / sizeof (menuitem_t),
&OPTIONS_DataDef,
0,
OPTIONS_DataScreenshot,
48, 80,
SKINCOLOR_BLUEBERRY, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};
void Screenshot_option_Onchange(void)
{
// Screenshot opt is at #3, 0 based array obv.
OPTIONS_DataScreenshot[2].status =
(cv_screenshot_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
}
void Moviemode_mode_Onchange(void)
{
#if 0
INT32 i, cstart, cend;
for (i = op_screenshot_gif_start; i <= op_screenshot_apng_end; ++i)
OP_ScreenshotOptionsMenu[i].status = IT_DISABLED;
switch (cv_moviemode.value)
{
case MM_GIF:
cstart = op_screenshot_gif_start;
cend = op_screenshot_gif_end;
break;
case MM_APNG:
cstart = op_screenshot_apng_start;
cend = op_screenshot_apng_end;
break;
default:
return;
}
for (i = cstart; i <= cend; ++i)
OP_ScreenshotOptionsMenu[i].status = IT_STRING|IT_CVAR;
#endif
}
void Moviemode_option_Onchange(void)
{
// opt 7 in a 0 based array, you get the idea...
OPTIONS_DataScreenshot[6].status =
(cv_movie_option.value == 3 ? IT_CVAR|IT_STRING|IT_CV_STRING : IT_DISABLED);
}

View file

@ -0,0 +1,60 @@
/// \file menus/options-gameplay-1.c
/// \brief Gameplay Options -- see gopt_e
#include "../k_menu.h"
menuitem_t OPTIONS_Gameplay[] =
{
{IT_STRING | IT_CVAR, "Game Speed", "Change Game Speed for the next map.",
NULL, {.cvar = &cv_kartspeed}, 0, 0},
{IT_STRING | IT_CVAR, "Base Lap Count", "Change how many laps must be completed per race.",
NULL, {.cvar = &cv_numlaps}, 0, 0},
{IT_STRING | IT_CVAR, "Frantic Items", "Make item odds crazier with more powerful items!",
NULL, {.cvar = &cv_kartfrantic}, 0, 0},
{IT_STRING | IT_CVAR, "Encore Mode", "Forces Encore Mode on for the next map.",
NULL, {.cvar = &cv_kartencore}, 0, 0},
{IT_STRING | IT_CVAR, "Exit Countdown", "How long players have to finish after 1st place finishes.",
NULL, {.cvar = &cv_countdowntime}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Time Limit", "Change the time limit for Battle rounds.",
NULL, {.cvar = &cv_timelimit}, 0, 0},
{IT_STRING | IT_CVAR, "Starting Bumpers", "Change how many bumpers player start with in Battle.",
NULL, {.cvar = &cv_kartbumpers}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Minimum Input Delay", "Practice for online play! Higher = more delay.",
NULL, {.cvar = &cv_mindelay}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Random Item Toggles...", "Change which items to enable for your games.",
NULL, {.submenu = &OPTIONS_GameplayItemsDef}, 0, 0},
};
menu_t OPTIONS_GameplayDef = {
sizeof (OPTIONS_Gameplay) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Gameplay,
48, 80,
SKINCOLOR_SCARLET, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,182 @@
/// \file menus/options-gameplay-item-toggles.c
/// \brief Random Item Toggles
#include "../k_menu.h"
#include "../s_sound.h"
menuitem_t OPTIONS_GameplayItems[] =
{
// Mostly handled by the drawing function.
{IT_KEYHANDLER | IT_NOTHING, NULL, "Super Rings", NULL, {.routine = M_HandleItemToggles}, KITEM_SUPERRING, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Self-Propelled Bombs", NULL, {.routine = M_HandleItemToggles}, KITEM_SPB, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Eggman Marks", NULL, {.routine = M_HandleItemToggles}, KITEM_EGGMAN, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Toggle All", NULL, {.routine = M_HandleItemToggles}, 0, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Sneakers", NULL, {.routine = M_HandleItemToggles}, KITEM_SNEAKER, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Sneakers x2", NULL, {.routine = M_HandleItemToggles}, KRITEM_DUALSNEAKER, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Sneakers x3", NULL, {.routine = M_HandleItemToggles}, KRITEM_TRIPLESNEAKER, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Rocket Sneakers", NULL, {.routine = M_HandleItemToggles}, KITEM_ROCKETSNEAKER, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Bananas", NULL, {.routine = M_HandleItemToggles}, KITEM_BANANA, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Bananas x3", NULL, {.routine = M_HandleItemToggles}, KRITEM_TRIPLEBANANA, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Proximity Mines", NULL, {.routine = M_HandleItemToggles}, KITEM_MINE, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Orbinauts", NULL, {.routine = M_HandleItemToggles}, KITEM_ORBINAUT, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Orbinauts x3", NULL, {.routine = M_HandleItemToggles}, KRITEM_TRIPLEORBINAUT, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Orbinauts x4", NULL, {.routine = M_HandleItemToggles}, KRITEM_QUADORBINAUT, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Land Mines", NULL, {.routine = M_HandleItemToggles}, KITEM_LANDMINE, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Jawz", NULL, {.routine = M_HandleItemToggles}, KITEM_JAWZ, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Jawz x2", NULL, {.routine = M_HandleItemToggles}, KRITEM_DUALJAWZ, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Ballhogs", NULL, {.routine = M_HandleItemToggles}, KITEM_BALLHOG, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Drop Targets", NULL, {.routine = M_HandleItemToggles}, KITEM_DROPTARGET, sfx_s258},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Lightning Shields", NULL, {.routine = M_HandleItemToggles}, KITEM_LIGHTNINGSHIELD, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Bubble Shields", NULL, {.routine = M_HandleItemToggles}, KITEM_BUBBLESHIELD, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Flame Shields", NULL, {.routine = M_HandleItemToggles}, KITEM_FLAMESHIELD, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Hyudoros", NULL, {.routine = M_HandleItemToggles}, KITEM_HYUDORO, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Invinciblity", NULL, {.routine = M_HandleItemToggles}, KITEM_INVINCIBILITY, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Grow", NULL, {.routine = M_HandleItemToggles}, KITEM_GROW, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Shrink", NULL, {.routine = M_HandleItemToggles}, KITEM_SHRINK, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, NULL, NULL, {.routine = M_HandleItemToggles}, 255, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Pogo Springs", NULL, {.routine = M_HandleItemToggles}, KITEM_POGOSPRING, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, "Kitchen Sinks", NULL, {.routine = M_HandleItemToggles}, KITEM_KITCHENSINK, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, NULL, NULL, {.routine = M_HandleItemToggles}, 255, 0},
{IT_KEYHANDLER | IT_NOTHING, NULL, NULL, NULL, {.routine = M_HandleItemToggles}, 255, 0}
};
menu_t OPTIONS_GameplayItemsDef = {
sizeof (OPTIONS_GameplayItems) / sizeof (menuitem_t),
&OPTIONS_GameplayDef,
0,
OPTIONS_GameplayItems,
14, 40,
SKINCOLOR_SCARLET, 0,
2, 5,
M_DrawItemToggles,
M_OptionsTick,
NULL,
NULL,
NULL,
};
void M_HandleItemToggles(INT32 choice)
{
const INT32 width = 8, height = 4;
INT32 column = itemOn/height, row = itemOn%height;
INT16 next;
UINT8 i;
boolean exitmenu = false;
const UINT8 pid = 0;
(void) choice;
if (menucmd[pid].dpad_lr > 0)
{
S_StartSound(NULL, sfx_s3k5b);
column++;
if (((column*height)+row) >= currentMenu->numitems)
column = 0;
next = min(((column*height)+row), currentMenu->numitems-1);
itemOn = next;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr < 0)
{
S_StartSound(NULL, sfx_s3k5b);
column--;
if (column < 0)
column = width-1;
if (((column*height)+row) >= currentMenu->numitems)
column--;
next = max(((column*height)+row), 0);
if (next >= currentMenu->numitems)
next = currentMenu->numitems-1;
itemOn = next;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud > 0)
{
S_StartSound(NULL, sfx_s3k5b);
row = (row+1) % height;
if (((column*height)+row) >= currentMenu->numitems)
row = 0;
next = min(((column*height)+row), currentMenu->numitems-1);
itemOn = next;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
S_StartSound(NULL, sfx_s3k5b);
row = (row-1) % height;
if (row < 0)
row = height-1;
if (((column*height)+row) >= currentMenu->numitems)
row--;
next = max(((column*height)+row), 0);
if (next >= currentMenu->numitems)
next = currentMenu->numitems-1;
itemOn = next;
M_SetMenuDelay(pid);
}
else if (M_MenuConfirmPressed(pid))
{
M_SetMenuDelay(pid);
if (currentMenu->menuitems[itemOn].mvar1 == 255)
{
//S_StartSound(NULL, sfx_s26d);
if (!shitsfree)
{
shitsfree = TICRATE;
S_StartSound(NULL, sfx_itfree);
}
}
else
if (currentMenu->menuitems[itemOn].mvar1 == 0)
{
INT32 v = cv_items[0].value;
S_StartSound(NULL, sfx_s1b4);
for (i = 0; i < NUMKARTRESULTS-1; i++)
{
if (cv_items[i].value == v)
CV_AddValue(&cv_items[i], 1);
}
}
else
{
if (currentMenu->menuitems[itemOn].mvar2)
{
S_StartSound(NULL, currentMenu->menuitems[itemOn].mvar2);
}
else
{
S_StartSound(NULL, sfx_s1ba);
}
CV_AddValue(&cv_items[currentMenu->menuitems[itemOn].mvar1-1], 1);
}
}
else if (M_MenuBackPressed(pid))
{
M_SetMenuDelay(pid);
exitmenu = true;
}
if (exitmenu)
{
if (currentMenu->prevMenu)
M_SetupNextMenu(currentMenu->prevMenu, false);
else
M_ClearMenus(true);
}
}

57
src/menus/options-hud-1.c Normal file
View file

@ -0,0 +1,57 @@
/// \file menus/options-hud-1.c
/// \brief HUD Options
#include "../k_menu.h"
#include "../r_main.h" // cv_showhud
#include "../v_video.h" // cv_constextsize
#include "../console.h" // console cvars
menuitem_t OPTIONS_HUD[] =
{
{IT_STRING | IT_CVAR, "Show HUD (F3)", "Toggles HUD display. Great for taking screenshots!",
NULL, {.cvar = &cv_showhud}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_SLIDER, "HUD Opacity", "Non opaque values may have performance impacts in software mode.",
NULL, {.cvar = &cv_translucenthud}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Speedometer", "Choose to what speed unit to display or toggle off the speedometer.",
NULL, {.cvar = &cv_kartspeedometer}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Console Text Size", "Size of the text within the console.",
NULL, {.cvar = &cv_constextsize}, 0, 0},
// we spell words properly here.
{IT_STRING | IT_CVAR, "Console Tint", "Change the background colour of the console.",
NULL, {.cvar = &cons_backcolor}, 0, 0},
{IT_STRING | IT_CVAR, "Show \"FOCUS LOST\"", "Displays \"FOCUS LOST\" when the game window isn't the active window.",
NULL, {.cvar = &cv_showfocuslost}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Online HUD Options...", "HUD options related to the online chat box and other features.",
NULL, {.submenu = &OPTIONS_HUDOnlineDef}, 0, 0},
};
menu_t OPTIONS_HUDDef = {
sizeof (OPTIONS_HUD) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_HUD,
48, 80,
SKINCOLOR_SUNSLAM, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,54 @@
/// \file menus/options-hud-inline.c
/// \brief Online HUD Options
#include "../k_menu.h"
menuitem_t OPTIONS_HUDOnline[] =
{
{IT_STRING | IT_CVAR, "Chat Mode", "Choose whether to display chat in its own window or the console.",
NULL, {.cvar = &cv_consolechat}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Chat Box Tint", "Changes the background colour of the chat box.",
NULL, {.cvar = &cv_chatbacktint}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_SLIDER, "Chat Box Width", "Change the width of the Chat Box",
NULL, {.cvar = &cv_chatwidth}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_SLIDER, "Chat Box Height", "Change the height of the Chat Box",
NULL, {.cvar = &cv_chatheight}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Message Fadeout Time", "How long chat messages stay displayed with the chat closed.",
NULL, {.cvar = &cv_chattime}, 0, 0},
{IT_STRING | IT_CVAR, "Spam Protection", "Prevents too many message from a single player from being displayed.",
NULL, {.cvar = &cv_chatspamprotection}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Local Ping Display", "In netgames, displays your ping at the lower right corner of the screen.",
NULL, {.cvar = &cv_showping}, 0, 0},
};
menu_t OPTIONS_HUDOnlineDef = {
sizeof (OPTIONS_HUDOnline) / sizeof (menuitem_t),
&OPTIONS_HUDDef,
0,
OPTIONS_HUDOnline,
48, 80,
SKINCOLOR_SUNSLAM, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,218 @@
/// \file menus/options-profiles-1.c
/// \brief Profiles Menu
#include "../k_menu.h"
#include "../s_sound.h"
// profile select
menuitem_t OPTIONS_Profiles[] = {
{IT_KEYHANDLER | IT_NOTHING, NULL, "Select a Profile.",
NULL, {.routine = M_HandleProfileSelect}, 0, 0}, // dummy menuitem for the control func
};
menu_t OPTIONS_ProfilesDef = {
sizeof (OPTIONS_Profiles) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Profiles,
32, 80,
SKINCOLOR_ULTRAMARINE, 0,
2, 5,
M_DrawProfileSelect,
M_OptionsTick,
NULL,
NULL,
NULL,
};
consvar_t cv_dummyprofilename = CVAR_INIT ("dummyprofilename", "", CV_HIDDEN, NULL, NULL);
consvar_t cv_dummyprofileplayername = CVAR_INIT ("dummyprofileplayername", "", CV_HIDDEN, NULL, NULL);
consvar_t cv_dummyprofilekickstart = CVAR_INIT ("dummyprofilekickstart", "Off", CV_HIDDEN, CV_OnOff, NULL);
void M_ProfileSelectInit(INT32 choice)
{
(void)choice;
optionsmenu.profilemenu = true;
M_SetupNextMenu(&OPTIONS_ProfilesDef, false);
}
// Select the current profile for menu use and go to maindef.
void M_FirstPickProfile(INT32 c)
{
if (c == MA_YES)
{
M_ResetOptions(); // Reset all options variables otherwise things are gonna go reaaal bad lol.
optionsmenu.profile = NULL; // Make sure to get rid of that, too.
PR_ApplyProfile(optionsmenu.profilen, 0);
M_SetupNextMenu(M_InterruptMenuWithChallenges(&MainDef), false);
// Tell the game this is the last profile we picked.
CV_StealthSetValue(&cv_ttlprofilen, optionsmenu.profilen);
// Save em!
PR_SaveProfiles();
return;
}
}
// Start menu edition. Call this with MA_YES if not used with a textbox.
static void M_StartEditProfile(INT32 c)
{
const INT32 maxp = PR_GetNumProfiles();
if (c == MA_YES)
{
if (optionsmenu.profilen == maxp)
PR_InitNewProfile(); // initialize the new profile.
optionsmenu.profile = PR_GetProfile(optionsmenu.profilen);
// copy this profile's controls into optionsmenu so that we can edit controls without changing them directly.
// we do this so that we don't edit a profile's controls in real-time and end up doing really weird shit.
memcpy(&optionsmenu.tempcontrols, optionsmenu.profile->controls, sizeof(gamecontroldefault));
// This is now used to move the card we've selected.
optionsmenu.optx = 160;
optionsmenu.opty = 35;
optionsmenu.toptx = 130/2;
optionsmenu.topty = 0;
// setup cvars
if (optionsmenu.profile->version)
{
CV_StealthSet(&cv_dummyprofilename, optionsmenu.profile->profilename);
CV_StealthSet(&cv_dummyprofileplayername, optionsmenu.profile->playername);
CV_StealthSetValue(&cv_dummyprofilekickstart, optionsmenu.profile->kickstartaccel);
}
else
{
CV_StealthSet(&cv_dummyprofilename, "");
CV_StealthSet(&cv_dummyprofileplayername, "");
CV_StealthSetValue(&cv_dummyprofilekickstart, 0); // off
}
// Setup greyout and stuff.
OPTIONS_EditProfile[popt_profilename].status = IT_STRING | IT_CVAR | IT_CV_STRING;
OPTIONS_EditProfile[popt_profilepname].status = IT_STRING | IT_CVAR | IT_CV_STRING;
OPTIONS_EditProfile[popt_char].status = IT_STRING | IT_CALL;
if (gamestate != GS_MENU) // If we're modifying things mid game, transtext some of those!
{
OPTIONS_EditProfile[popt_profilename].status |= IT_TRANSTEXT;
OPTIONS_EditProfile[popt_profilepname].status |= IT_TRANSTEXT;
OPTIONS_EditProfile[popt_char].status |= IT_TRANSTEXT;
}
M_SetupNextMenu(&OPTIONS_EditProfileDef, false);
return;
}
}
void M_HandleProfileSelect(INT32 ch)
{
const UINT8 pid = 0;
INT32 maxp = PR_GetNumProfiles();
boolean creatable = (maxp < MAXPROFILES);
(void) ch;
if (!creatable)
{
maxp = MAXPROFILES;
}
if (menucmd[pid].dpad_lr > 0)
{
optionsmenu.profilen++;
optionsmenu.offset += (128 + 128/8);
if (optionsmenu.profilen > maxp)
{
optionsmenu.profilen = 0;
optionsmenu.offset -= (128 + 128/8)*(maxp+1);
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr < 0)
{
optionsmenu.profilen--;
optionsmenu.offset -= (128 + 128/8);
if (optionsmenu.profilen < 0)
{
optionsmenu.profilen = maxp;
optionsmenu.offset += (128 + 128/8)*(maxp+1);
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (M_MenuConfirmPressed(pid))
{
// Boot profile setup has already been done.
if (cv_currprofile.value > -1)
{
if (optionsmenu.profilen == 0) // Guest profile, you can't edit that one!
{
S_StartSound(NULL, sfx_s3k7b);
M_StartMessage(M_GetText("The Guest profile cannot be edited.\nCreate a new profile instead."), NULL, MM_NOTHING);
M_SetMenuDelay(pid);
return;
}
else if (creatable && optionsmenu.profilen == maxp && gamestate != GS_MENU)
{
S_StartSound(NULL, sfx_s3k7b);
M_StartMessage(M_GetText("Cannot create a new profile\nmid-game. Return to the\ntitle screen first."), NULL, MM_NOTHING);
M_SetMenuDelay(pid);
return;
}
S_StartSound(NULL, sfx_s3k5b);
M_StartEditProfile(MA_YES);
}
else
{
// We're on the profile selection screen.
if (creatable && optionsmenu.profilen == maxp)
{
M_StartEditProfile(MA_YES);
M_SetMenuDelay(pid);
return;
}
else
{
#if 0
if (optionsmenu.profilen == 0)
{
M_StartMessage(M_GetText("Are you sure you wish\nto use the Guest Profile?\nThis profile cannot be customised.\nIt is recommended to create\na new Profile instead.\n\nPress (A) to confirm or (B) to cancel"), FUNCPTRCAST(M_FirstPickProfile), MM_YESNO);
return;
}
#endif
M_FirstPickProfile(MA_YES);
M_SetMenuDelay(pid);
return;
}
}
}
else if (M_MenuBackPressed(pid))
{
optionsmenu.resetprofilemenu = true;
M_GoBack(0);
M_SetMenuDelay(pid);
}
if (menutransition.tics == 0 && optionsmenu.resetprofile)
{
optionsmenu.profile = NULL; // Make sure to reset that when transitions are done.'
optionsmenu.resetprofile = false;
}
}

View file

@ -0,0 +1,180 @@
/// \file menus/options-profiles-edit-1.c
/// \brief Profile Editor
#include "../k_menu.h"
#include "../s_sound.h"
menuitem_t OPTIONS_EditProfile[] = {
{IT_STRING | IT_CVAR | IT_CV_STRING, "Profile Name", "6-character long name to identify this Profile.",
NULL, {.cvar = &cv_dummyprofilename}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_STRING, "Player Name", "Name displayed online when using this Profile.",
NULL, {.cvar = &cv_dummyprofileplayername}, 0, 0},
{IT_STRING | IT_CALL, "Character", "Default character and color for this Profile.",
NULL, {.routine = M_CharacterSelect}, 0, 0},
{IT_STRING | IT_CALL, "Controls", "Select the button mappings for this Profile.",
NULL, {.routine = M_ProfileDeviceSelect}, 0, 0},
{IT_STRING | IT_CALL, "Confirm", "Confirm changes.",
NULL, {.routine = M_ConfirmProfile}, 0, 0},
};
menu_t OPTIONS_EditProfileDef = {
sizeof (OPTIONS_EditProfile) / sizeof (menuitem_t),
&OPTIONS_ProfilesDef,
0,
OPTIONS_EditProfile,
32, 80,
SKINCOLOR_ULTRAMARINE, 0,
2, 5,
M_DrawEditProfile,
M_HandleProfileEdit,
NULL,
NULL,
M_ProfileEditInputs,
};
// Returns true if the profile can be saved, false otherwise. Also starts messages if necessary.
static boolean M_ProfileEditEnd(const UINT8 pid)
{
UINT8 i;
// Guest profile, you can't edit that one!
if (optionsmenu.profilen == 0)
{
S_StartSound(NULL, sfx_s3k7b);
M_StartMessage(M_GetText("Guest profile cannot be edited.\nCreate a new profile instead."), NULL, MM_NOTHING);
M_SetMenuDelay(pid);
return false;
}
// check if some profiles have the same name
for (i = 0; i < PR_GetNumProfiles(); i++)
{
profile_t *check = PR_GetProfile(i);
// For obvious reasons don't check if our name is the same as our name....
if (check != optionsmenu.profile)
{
if (!(strcmp(optionsmenu.profile->profilename, check->profilename)))
{
S_StartSound(NULL, sfx_s3k7b);
M_StartMessage(M_GetText("Another profile uses the same name.\nThis must be changed to be able to save."), NULL, MM_NOTHING);
M_SetMenuDelay(pid);
return false;
}
}
}
return true;
}
static void M_ProfileEditExit(void)
{
optionsmenu.toptx = 160;
optionsmenu.topty = 35;
optionsmenu.resetprofile = true; // Reset profile after the transition is done.
PR_SaveProfiles(); // save profiles after we do that.
}
// For profile edit, just make sure going back resets the card to its position, the rest is taken care of automatically.
boolean M_ProfileEditInputs(INT32 ch)
{
(void) ch;
const UINT8 pid = 0;
if (M_MenuBackPressed(pid))
{
if (M_ProfileEditEnd(pid))
{
M_ProfileEditExit();
if (cv_currprofile.value == -1)
M_SetupNextMenu(&MAIN_ProfilesDef, false);
else
M_GoBack(0);
M_SetMenuDelay(pid);
}
return true;
}
else if (M_MenuConfirmPressed(pid))
{
if (currentMenu->menuitems[itemOn].status & IT_TRANSTEXT)
return true; // No.
}
return false;
}
// Handle some actions in profile editing
void M_HandleProfileEdit(void)
{
// Always copy the profile name and player name in the profile.
if (optionsmenu.profile)
{
// Copy the first 6 chars for profile name
if (strlen(cv_dummyprofilename.string))
{
char *s;
// convert dummyprofilename to uppercase
strncpy(optionsmenu.profile->profilename, cv_dummyprofilename.string, PROFILENAMELEN);
s = optionsmenu.profile->profilename;
while (*s)
{
*s = toupper(*s);
s++;
}
}
if (strlen(cv_dummyprofileplayername.string))
strncpy(optionsmenu.profile->playername, cv_dummyprofileplayername.string, MAXPLAYERNAME);
}
M_OptionsTick(); // Has to be afterwards because this can unset optionsmenu.profile
}
// Confirm Profile edi via button.
void M_ConfirmProfile(INT32 choice)
{
const UINT8 pid = 0;
(void) choice;
if (M_ProfileEditEnd(pid))
{
if (cv_currprofile.value > -1)
{
M_ProfileEditExit();
M_GoBack(0);
M_SetMenuDelay(pid);
}
else
{
M_StartMessage(M_GetText("Are you sure you wish to\nselect this profile?\n\nPress (A) to confirm or (B) to cancel"), FUNCPTRCAST(M_FirstPickProfile), MM_YESNO);
M_SetMenuDelay(pid);
}
}
return;
}
// Prompt a device selection window (just tap any button on the device you want)
void M_ProfileDeviceSelect(INT32 choice)
{
(void)choice;
// While we're here, setup the incoming controls menu to reset the scroll & bind status:
optionsmenu.controlscroll = 0;
optionsmenu.bindcontrol = 0;
optionsmenu.bindtimer = 0;
optionsmenu.lastkey = 0;
optionsmenu.keyheldfor = 0;
optionsmenu.contx = optionsmenu.tcontx = controlleroffsets[gc_a][0];
optionsmenu.conty = optionsmenu.tconty = controlleroffsets[gc_a][1];
M_SetupNextMenu(&OPTIONS_ProfileControlsDef, false); // Don't set device here anymore.
}

View file

@ -0,0 +1,448 @@
/// \file menus/options-profiles-edit-controls.c
/// \brief Profile Controls Editor
#include "../k_menu.h"
#include "../s_sound.h"
#include "../i_joy.h" // for joystick menu controls
menuitem_t OPTIONS_ProfileControls[] = {
{IT_HEADER, "MAIN CONTROLS", "That's the stuff on the controller!!",
NULL, {NULL}, 0, 0},
{IT_CONTROL, "A", "Accelerate / Confirm",
"PR_BTA", {.routine = M_ProfileSetControl}, gc_a, 0},
{IT_CONTROL, "B", "Look backwards / Back",
"PR_BTB", {.routine = M_ProfileSetControl}, gc_b, 0},
{IT_CONTROL, "C", "Spindash / Extra",
"PR_BTC", {.routine = M_ProfileSetControl}, gc_c, 0},
{IT_CONTROL, "X", "Brake / Back",
"PR_BTX", {.routine = M_ProfileSetControl}, gc_x, 0},
// @TODO What does this do???
{IT_CONTROL, "Y", "N/A ?",
"PR_BTY", {.routine = M_ProfileSetControl}, gc_y, 0},
{IT_CONTROL, "Z", "N/A ?",
"PR_BTZ", {.routine = M_ProfileSetControl}, gc_z, 0},
{IT_CONTROL, "L", "Use item",
"PR_BTL", {.routine = M_ProfileSetControl}, gc_l, 0},
{IT_CONTROL, "R", "Drift",
"PR_BTR", {.routine = M_ProfileSetControl}, gc_r, 0},
{IT_CONTROL, "Turn Left", "Turn left",
"PR_PADL", {.routine = M_ProfileSetControl}, gc_left, 0},
{IT_CONTROL, "Turn Right", "Turn right",
"PR_PADR", {.routine = M_ProfileSetControl}, gc_right, 0},
{IT_CONTROL, "Aim Forward", "Aim forwards",
"PR_PADU", {.routine = M_ProfileSetControl}, gc_up, 0},
{IT_CONTROL, "Aim Backwards", "Aim backwards",
"PR_PADD", {.routine = M_ProfileSetControl}, gc_down, 0},
{IT_CONTROL, "Start", "Open pause menu",
"PR_BTS", {.routine = M_ProfileSetControl}, gc_start, 0},
{IT_HEADER, "OPTIONAL CONTROLS", "Take a screenshot, chat...",
NULL, {NULL}, 0, 0},
{IT_CONTROL, "SCREENSHOT", "Also usable with F8 on Keyboard.",
NULL, {.routine = M_ProfileSetControl}, gc_screenshot, 0},
{IT_CONTROL, "GIF CAPTURE", "Also usable with F9 on Keyboard.",
NULL, {.routine = M_ProfileSetControl}, gc_recordgif, 0},
{IT_CONTROL, "OPEN CHAT", "Opens chatbox in online games.",
NULL, {.routine = M_ProfileSetControl}, gc_talk, 0},
{IT_CONTROL, "OPEN TEAM CHAT", "Do we even have team gamemodes?",
NULL, {.routine = M_ProfileSetControl}, gc_teamtalk, 0},
{IT_CONTROL, "SHOW RANKINGS", "Show mid-game rankings.",
NULL, {.routine = M_ProfileSetControl}, gc_rankings, 0},
{IT_CONTROL, "OPEN CONSOLE", "Opens the developer options console.",
NULL, {.routine = M_ProfileSetControl}, gc_console, 0},
{IT_CONTROL, "LUA/A", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_luaa, 0},
{IT_CONTROL, "LUA/B", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_luab, 0},
{IT_CONTROL, "LUA/C", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_luac, 0},
{IT_HEADER, "TOGGLES", "For per-player commands",
NULL, {NULL}, 0, 0},
{IT_CONTROL | IT_CVAR, "KICKSTART ACCEL", "Hold A to auto-accel. Tap it to cancel.",
NULL, {.cvar = &cv_dummyprofilekickstart}, 0, 0},
{IT_HEADER, "EXTRA", "",
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "TRY MAPPINGS", "Test your controls.",
NULL, {.routine = M_ProfileTryController}, 0, 0},
{IT_STRING | IT_CALL, "CONFIRM", "Go back to profile setup.",
NULL, {.routine = M_ProfileControlsConfirm}, 0, 0},
};
menu_t OPTIONS_ProfileControlsDef = {
sizeof (OPTIONS_ProfileControls) / sizeof (menuitem_t),
&OPTIONS_EditProfileDef,
0,
OPTIONS_ProfileControls,
32, 80,
SKINCOLOR_ULTRAMARINE, 0,
3, 5,
M_DrawProfileControls,
M_HandleProfileControls,
NULL,
NULL,
M_ProfileControlsInputs,
};
// sets whatever device has had its key pressed to the active device.
// 20/05/22: Commented out for now but not deleted as it might still find some use in the future?
/*
static void SetDeviceOnPress(void)
{
UINT8 i;
for (i=0; i < MAXDEVICES; i++)
{
if (deviceResponding[i])
{
CV_SetValue(&cv_usejoystick[0], i); // Force-set this joystick as the current joystick we're using for P1 (which is the only one controlling menus)
CONS_Printf("SetDeviceOnPress: Device for %d set to %d\n", 0, i);
return;
}
}
}
*/
void M_HandleProfileControls(void)
{
UINT8 maxscroll = currentMenu->numitems - 5;
M_OptionsTick();
optionsmenu.contx += (optionsmenu.tcontx - optionsmenu.contx)/2;
optionsmenu.conty += (optionsmenu.tconty - optionsmenu.conty)/2;
if (abs(optionsmenu.contx - optionsmenu.tcontx) < 2 && abs(optionsmenu.conty - optionsmenu.tconty) < 2)
{
optionsmenu.contx = optionsmenu.tcontx;
optionsmenu.conty = optionsmenu.tconty; // Avoid awkward 1 px errors.
}
optionsmenu.controlscroll = itemOn - 3; // very barebones scrolling, but it works just fine for our purpose.
if (optionsmenu.controlscroll > maxscroll)
optionsmenu.controlscroll = maxscroll;
if (optionsmenu.controlscroll < 0)
optionsmenu.controlscroll = 0;
// bindings, cancel if timer is depleted.
if (optionsmenu.bindcontrol)
{
optionsmenu.bindtimer--;
if (!optionsmenu.bindtimer)
{
optionsmenu.bindcontrol = 0; // we've gone past the max, just stop.
}
}
}
void M_ProfileTryController(INT32 choice)
{
(void)choice;
optionsmenu.trycontroller = TICRATE*5;
// Apply these controls right now on P1's end.
memcpy(&gamecontrol[0], optionsmenu.tempcontrols, sizeof(gamecontroldefault));
}
static void M_ProfileControlSaveResponse(INT32 choice)
{
if (choice == MA_YES)
{
SINT8 belongsto = PR_ProfileUsedBy(optionsmenu.profile);
// Save the profile
optionsmenu.profile->kickstartaccel = cv_dummyprofilekickstart.value;
memcpy(&optionsmenu.profile->controls, optionsmenu.tempcontrols, sizeof(gamecontroldefault));
// If this profile is in-use by anyone, apply the changes immediately upon exiting.
// Don't apply the profile itself as that would lead to issues mid-game.
if (belongsto > -1 && belongsto < MAXSPLITSCREENPLAYERS)
{
memcpy(&gamecontrol[belongsto], optionsmenu.tempcontrols, sizeof(gamecontroldefault));
CV_StealthSetValue(&cv_kickstartaccel[belongsto], cv_dummyprofilekickstart.value);
}
M_GoBack(0);
}
}
void M_ProfileControlsConfirm(INT32 choice)
{
(void)choice;
//M_StartMessage(M_GetText("Exiting will save the control changes\nfor this Profile.\nIs this okay?\n\nPress (A) to confirm or (B) to cancel"), FUNCPTRCAST(M_ProfileControlSaveResponse), MM_YESNO);
// TODO: Add a graphic for controls saving, instead of obnoxious prompt.
M_ProfileControlSaveResponse(MA_YES);
optionsmenu.profile->kickstartaccel = cv_dummyprofilekickstart.value; // Make sure to save kickstart accel.
// Reapply player 1's real profile.
if (cv_currprofile.value > -1)
{
PR_ApplyProfile(cv_lastprofile[0].value, 0);
}
}
boolean M_ProfileControlsInputs(INT32 ch)
{
const UINT8 pid = 0;
(void)ch;
// By default, accept all inputs.
if (optionsmenu.trycontroller)
{
if (menucmd[pid].dpad_ud || menucmd[pid].dpad_lr || menucmd[pid].buttons)
{
optionsmenu.trycontroller = 5*TICRATE;
}
else
{
optionsmenu.trycontroller--;
}
if (optionsmenu.trycontroller == 0)
{
// Reset controls to that of the current profile.
profile_t *cpr = PR_GetProfile(cv_currprofile.value);
if (cpr == NULL)
cpr = PR_GetProfile(0); // Creating a profile at boot, revert to guest profile
memcpy(&gamecontrol[0], cpr->controls, sizeof(gamecontroldefault));
}
return true;
}
if (optionsmenu.bindcontrol)
return true; // Eat all inputs there. We'll use a stupid hack in M_Responder instead.
//SetDeviceOnPress(); // Update device constantly so that we don't stay stuck with otpions saying a device is unavailable just because we're mapping multiple devices...
if (M_MenuExtraPressed(pid))
{
// check if we're on a valid menu option...
if (currentMenu->menuitems[itemOn].mvar1)
{
// clear controls for that key
INT32 i;
for (i = 0; i < MAXINPUTMAPPING; i++)
optionsmenu.tempcontrols[currentMenu->menuitems[itemOn].mvar1][i] = KEY_NULL;
S_StartSound(NULL, sfx_s3k66);
}
M_SetMenuDelay(pid);
return true;
}
else if (M_MenuBackPressed(pid))
{
M_ProfileControlsConfirm(0);
M_SetMenuDelay(pid);
return true;
}
return false;
}
void M_ProfileSetControl(INT32 ch)
{
INT32 controln = currentMenu->menuitems[itemOn].mvar1;
UINT8 i;
(void) ch;
optionsmenu.bindcontrol = 1; // Default to control #1
for (i = 0; i < MAXINPUTMAPPING; i++)
{
if (optionsmenu.tempcontrols[controln][i] == KEY_NULL)
{
optionsmenu.bindcontrol = i+1;
break;
}
}
// If we could find a null key to map into, map there.
// Otherwise, this will stay at 1 which means we'll overwrite the first bound control.
optionsmenu.bindtimer = TICRATE*5;
}
// Map the event to the profile.
#define KEYHOLDFOR 1
void M_MapProfileControl(event_t *ev)
{
INT32 c = 0;
UINT8 n = optionsmenu.bindcontrol-1; // # of input to bind
INT32 controln = currentMenu->menuitems[itemOn].mvar1; // gc_
UINT8 where = n; // By default, we'll save the bind where we're supposed to map.
INT32 i;
//SetDeviceOnPress(); // Update cv_usejoystick
// Only consider keydown and joystick events to make sure we ignore ev_mouse and other events
// See also G_MapEventsToControls
switch (ev->type)
{
case ev_keydown:
if (ev->data1 < NUMINPUTS)
{
c = ev->data1;
}
#ifdef PARANOIA
else
{
CONS_Debug(DBG_GAMELOGIC, "Bad downkey input %d\n", ev->data1);
}
#endif
break;
case ev_joystick:
if (ev->data1 >= JOYAXES)
{
#ifdef PARANOIA
CONS_Debug(DBG_GAMELOGIC, "Bad joystick axis event %d\n", ev->data1);
#endif
return;
}
else
{
INT32 deadzone = deadzone = (JOYAXISRANGE * cv_deadzone[0].value) / FRACUNIT; // TODO how properly account for different deadzone cvars for different devices
boolean responsivelr = ((ev->data2 != INT32_MAX) && (abs(ev->data2) >= deadzone));
boolean responsiveud = ((ev->data3 != INT32_MAX) && (abs(ev->data3) >= deadzone));
i = ev->data1;
if (i >= JOYANALOGS)
{
// The trigger axes are handled specially.
i -= JOYANALOGS;
if (responsivelr)
{
c = KEY_AXIS1 + (JOYANALOGS * 4) + (i * 2);
}
else if (responsiveud)
{
c = KEY_AXIS1 + (JOYANALOGS * 4) + (i * 2) + 1;
}
}
else
{
// Actual analog sticks
// Only consider unambiguous assignment.
if (responsivelr == responsiveud)
return;
if (responsivelr)
{
if (ev->data2 < 0)
{
// Left
c = KEY_AXIS1 + (i * 4);
}
else
{
// Right
c = KEY_AXIS1 + (i * 4) + 1;
}
}
else //if (responsiveud)
{
if (ev->data3 < 0)
{
// Up
c = KEY_AXIS1 + (i * 4) + 2;
}
else
{
// Down
c = KEY_AXIS1 + (i * 4) + 3;
}
}
}
}
break;
default:
return;
}
// safety result
if (!c)
return;
// Set menu delay regardless of what we're doing to avoid stupid stuff.
M_SetMenuDelay(0);
// Check if this particular key (c) is already bound in any slot.
// If that's the case, simply do nothing.
for (i = 0; i < MAXINPUTMAPPING; i++)
{
if (optionsmenu.tempcontrols[controln][i] == c)
{
optionsmenu.bindcontrol = 0;
return;
}
}
// With the way we do things, there cannot be instances of 'gaps' within the controls, so we don't need to pretend like we need to handle that.
// Unless of course you tamper with the cfg file, but then it's *your* fault, not mine.
optionsmenu.tempcontrols[controln][where] = c;
optionsmenu.bindcontrol = 0; // not binding anymore
// If possible, reapply the profile...
// 19/05/22: Actually, no, don't do that, it just fucks everything up in too many cases.
/*
if (gamestate == GS_MENU) // In menu? Apply this to P1, no questions asked.
{
// Apply the profile's properties to player 1 but keep the last profile cv to p1's ACTUAL profile to revert once we exit.
UINT8 lastp = cv_lastprofile[0].value;
PR_ApplyProfile(PR_GetProfileNum(optionsmenu.profile), 0);
CV_StealthSetValue(&cv_lastprofile[0], lastp);
}
else // != GS_MENU
{
// ONLY apply the profile if it's in use by anything currently.
UINT8 pnum = PR_GetProfileNum(optionsmenu.profile);
for (i = 0; i < MAXSPLITSCREENPLAYERS; i++)
{
if (cv_lastprofile[i].value == pnum)
{
PR_ApplyProfile(pnum, i);
break;
}
}
}
*/
}
#undef KEYHOLDFOR

View file

@ -0,0 +1,64 @@
/// \file menus/options-server-1.c
/// \brief Server Options
#include "../k_menu.h"
menuitem_t OPTIONS_Server[] =
{
{IT_STRING | IT_CVAR | IT_CV_STRING, "Server Name", "Change the name of your server.",
NULL, {.cvar = &cv_servername}, 0, 0},
{IT_STRING | IT_CVAR, "Intermission", "Set how long to stay on the result screen.",
NULL, {.cvar = &cv_inttime}, 0, 0},
{IT_STRING | IT_CVAR, "Map Progression", "Set how the next map is chosen.",
NULL, {.cvar = &cv_advancemap}, 0, 0},
{IT_STRING | IT_CVAR, "Vote Timer", "Set how long players have to vote.",
NULL, {.cvar = &cv_votetime}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Maximum Players", "How many players can play at once.",
NULL, {.cvar = &cv_maxplayers}, 0, 0},
{IT_STRING | IT_CVAR, "Maximum Connections", "How many players & spectators can connect to the server.",
NULL, {.cvar = &cv_maxconnections}, 0, 0},
{IT_STRING | IT_CVAR, "Allow Joining", "Sets whether players can connect to your server.",
NULL, {.cvar = &cv_allownewplayer}, 0, 0},
{IT_STRING | IT_CVAR, "Allow Downloads", "Allows joiners to download missing files from you.",
NULL, {.cvar = &cv_downloading}, 0, 0},
{IT_STRING | IT_CVAR, "Pause Permissions", "Sets who can pause the game.",
NULL, {.cvar = &cv_pause}, 0, 0},
{IT_STRING | IT_CVAR, "Mute Chat", "Prevents non-admins from sending chat messages.",
NULL, {.cvar = &cv_mute}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Advanced...", "Advanced options. Be careful when messing with these!",
NULL, {.submenu = &OPTIONS_ServerAdvancedDef}, 0, 0},
};
menu_t OPTIONS_ServerDef = {
sizeof (OPTIONS_Server) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Server,
48, 70, // This menu here is slightly higher because there's a lot of options...
SKINCOLOR_VIOLET, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,62 @@
/// \file menus/options-server-advanced.c
/// \brief Advanced Server Options
#include "../k_menu.h"
menuitem_t OPTIONS_ServerAdvanced[] =
{
{IT_STRING | IT_CVAR | IT_CV_STRING, "Server Browser Address", "Default is \'https://ms.kartkrew.org/ms/api\'",
NULL, {.cvar = &cv_masterserver}, 0, 0},
{IT_STRING | IT_CVAR, "Resynch. Attempts", "How many times to attempt sending data to desynchronized players.",
NULL, {.cvar = &cv_resynchattempts}, 0, 0},
{IT_STRING | IT_CVAR, "Ping Limit (ms)", "Players above the ping limit will get kicked from the server.",
NULL, {.cvar = &cv_maxping}, 0, 0},
{IT_STRING | IT_CVAR, "Ping Timeout (s)", "Players must be above the ping limit for this long before being kicked.",
NULL, {.cvar = &cv_pingtimeout}, 0, 0},
{IT_STRING | IT_CVAR, "Connection Timeout (tics)", "Players not giving any netowrk activity for this long are kicked.",
NULL, {.cvar = &cv_nettimeout}, 0, 0},
{IT_STRING | IT_CVAR, "Join Timeout (tics)", "Players taking too long to join are kicked.",
NULL, {.cvar = &cv_jointimeout}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Max File Transfer", "Maximum size of the files that can be downloaded from joining clients. (KB)",
NULL, {.cvar = &cv_maxsend}, 0, 0},
{IT_STRING | IT_CVAR, "File Transfer Speed", "File transfer packet rate. Larger values send more data.",
NULL, {.cvar = &cv_downloadspeed}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Log Joiner IPs", "Shows the IP of connecting players.",
NULL, {.cvar = &cv_showjoinaddress}, 0, 0},
{IT_STRING | IT_CVAR, "Log Resynch", "Shows which players need resynchronization.",
NULL, {.cvar = &cv_blamecfail}, 0, 0},
{IT_STRING | IT_CVAR, "Log Transfers", "Shows when clients are downloading files from you.",
NULL, {.cvar = &cv_noticedownload}, 0, 0},
};
menu_t OPTIONS_ServerAdvancedDef = {
sizeof (OPTIONS_ServerAdvanced) / sizeof (menuitem_t),
&OPTIONS_ServerDef,
0,
OPTIONS_ServerAdvanced,
48, 70, // This menu here is slightly higher because there's a lot of options...
SKINCOLOR_VIOLET, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

66
src/menus/options-sound.c Normal file
View file

@ -0,0 +1,66 @@
/// \file menus/options-sound.c
/// \brief Sound Options
#include "../k_menu.h"
#include "../s_sound.h" // sounds consvars
#include "../g_game.h" // cv_chatnotifications
menuitem_t OPTIONS_Sound[] =
{
{IT_STRING | IT_CVAR, "SFX", "Enable or disable sound effect playback.",
NULL, {.cvar = &cv_gamesounds}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_SLIDER, "SFX Volume", "Adjust the volume of sound effects.",
NULL, {.cvar = &cv_soundvolume}, 0, 0},
{IT_STRING | IT_CVAR, "Music", "Enable or disable music playback.",
NULL, {.cvar = &cv_gamedigimusic}, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_SLIDER, "Music Volume", "Adjust the volume of music playback.",
NULL, {.cvar = &cv_digmusicvolume}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Reverse L/R Channels", "Reverse left & right channels for Stereo playback.",
NULL, {.cvar = &stereoreverse}, 0, 0},
{IT_STRING | IT_CVAR, "Surround", "Enables or disable Surround sound playback.",
NULL, {.cvar = &surround}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Chat Notifications", "Set when to play notification sounds when chat messages are received.",
NULL, {.cvar = &cv_chatnotifications}, 0, 0},
{IT_STRING | IT_CVAR, "Character Voices", "Set how often to play character voices in game.",
NULL, {.cvar = &cv_kartvoices}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Play Music While Unfocused", "Keeps playing music even if the game is not the active window.",
NULL, {.cvar = &cv_playmusicifunfocused}, 0, 0},
{IT_STRING | IT_CVAR, "Play SFX While Unfocused", "Keeps playing sound effects even if the game is not the active window.",
NULL, {.cvar = &cv_playsoundifunfocused}, 0, 0},
// @TODO: Sound test (there's currently no space on this menu, might be better to throw it in extras?)
};
menu_t OPTIONS_SoundDef = {
sizeof (OPTIONS_Sound) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Sound,
48, 80,
SKINCOLOR_THUNDER, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,68 @@
/// \file menus/options-video-1.c
/// \brief Video Options
#include "../k_menu.h"
#include "../r_main.h" // cv_skybox
#include "../v_video.h" // cv_globalgamma
#include "../r_fps.h" // fps cvars
// options menu
menuitem_t OPTIONS_Video[] =
{
{IT_STRING | IT_CALL, "Set Resolution...", "Change the screen resolution for the game.",
NULL, {.routine = M_VideoModeMenu}, 0, 0},
// A check to see if you're not running on a fucking antique potato powered stone i guess???????
#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
{IT_STRING | IT_CVAR, "Fullscreen", "Set whether you want to use fullscreen or windowed mode.",
NULL, {.cvar = &cv_fullscreen}, 0, 0},
#endif
{IT_NOTHING|IT_SPACE, NULL, "Kanade best waifu! I promise!",
NULL, {NULL}, 0, 0},
// Everytime I see a screenshot at max gamma I die inside
{IT_STRING | IT_CVAR | IT_CV_SLIDER, "Gamma", "Adjusts the overall brightness of the game.",
NULL, {.cvar = &cv_globalgamma}, 0, 0},
{IT_STRING | IT_CVAR, "FPS Cap", "Handles the refresh rate of the game (does not affect gamelogic).",
NULL, {.cvar = &cv_fpscap}, 0, 0},
{IT_STRING | IT_CVAR, "Enable Skyboxes", "Turning this off will improve performance at the detriment of visuals for many maps.",
NULL, {.cvar = &cv_skybox}, 0, 0},
{IT_STRING | IT_CVAR, "Draw Distance", "How far objects can be drawn. Lower values may improve performance at the cost of visibility.",
NULL, {.cvar = &cv_drawdist}, 0, 0},
{IT_STRING | IT_CVAR, "Weather Draw Distance", "Affects how far weather visuals can be drawn. Lower values improve performance.",
NULL, {.cvar = &cv_drawdist_precip}, 0, 0},
{IT_STRING | IT_CVAR, "Show FPS", "Displays the game framerate at the lower right corner of the screen.",
NULL, {.cvar = &cv_ticrate}, 0, 0},
{IT_NOTHING|IT_SPACE, NULL, "Kanade best waifu! I promise!",
NULL, {NULL}, 0, 0},
#ifdef HWRENDER
{IT_STRING | IT_SUBMENU, "Hardware Options...", "For usage and configuration of the OpenGL renderer.",
NULL, {.submenu = &OPTIONS_VideoOGLDef}, 0, 0},
#endif
};
menu_t OPTIONS_VideoDef = {
sizeof (OPTIONS_Video) / sizeof (menuitem_t),
&OPTIONS_MainDef,
0,
OPTIONS_Video,
32, 80,
SKINCOLOR_PLAGUE, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,60 @@
/// \file menus/options-video-gl.c
/// \brief OpenGL Options
#include "../k_menu.h"
#include "../hardware/hw_main.h" // gl consvars
menuitem_t OPTIONS_VideoOGL[] =
{
{IT_STRING | IT_CVAR, "Renderer", "Change renderers between Software and OpenGL",
NULL, {.cvar = &cv_renderer}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_HEADER, "OPTIONS BELOW ARE OPENGL ONLY!", "Watch people get confused anyway!!",
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "3D Models", "Use 3D models instead of sprites when applicable.",
NULL, {.cvar = &cv_glmodels}, 0, 0},
{IT_STRING | IT_CVAR, "Shaders", "Use GLSL Shaders. Turning them off increases performance at the expanse of visual quality.",
NULL, {.cvar = &cv_glshaders}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Texture Quality", "Texture depth. Higher values are recommended.",
NULL, {.cvar = &cv_scr_depth}, 0, 0},
{IT_STRING | IT_CVAR, "Texture Filter", "Texture Filter. Nearest is recommended.",
NULL, {.cvar = &cv_glfiltermode}, 0, 0},
{IT_STRING | IT_CVAR, "Anisotropic", "Lower values will improve performance at a minor quality loss.",
NULL, {.cvar = &cv_glanisotropicmode}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CVAR, "Sprite Billboarding", "Adjusts sprites when viewed from above or below to not make them appear flat.",
NULL, {.cvar = &cv_glspritebillboarding}, 0, 0},
{IT_STRING | IT_CVAR, "Software Perspective", "Emulates Software shearing when looking up or down. Not recommended.",
NULL, {.cvar = &cv_glshearing}, 0, 0},
};
menu_t OPTIONS_VideoOGLDef = {
sizeof (OPTIONS_VideoOGL) / sizeof (menuitem_t),
&OPTIONS_VideoDef,
0,
OPTIONS_VideoOGL,
32, 80,
SKINCOLOR_PLAGUE, 0,
2, 5,
M_DrawGenericOptions,
M_OptionsTick,
NULL,
NULL,
NULL,
};

View file

@ -0,0 +1,194 @@
/// \file menus/options-video-modes.c
/// \brief Video modes (resolutions)
#include "../k_menu.h"
#include "../i_video.h"
#include "../s_sound.h"
menuitem_t OPTIONS_VideoModes[] = {
{IT_KEYHANDLER | IT_NOTHING, NULL, "Select a resolution.",
NULL, {.routine = M_HandleVideoModes}, 0, 0}, // dummy menuitem for the control func
};
menu_t OPTIONS_VideoModesDef = {
sizeof (OPTIONS_VideoModes) / sizeof (menuitem_t),
&OPTIONS_VideoDef,
0,
OPTIONS_VideoModes,
48, 80,
SKINCOLOR_PLAGUE, 0,
2, 5,
M_DrawVideoModes,
M_OptionsTick,
NULL,
NULL,
NULL,
};
// setup video mode menu
void M_VideoModeMenu(INT32 choice)
{
INT32 i, j, vdup, nummodes;
UINT32 width, height;
const char *desc;
(void)choice;
memset(optionsmenu.modedescs, 0, sizeof(optionsmenu.modedescs));
#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
VID_PrepareModeList(); // FIXME: hack
#endif
optionsmenu.vidm_nummodes = 0;
optionsmenu.vidm_selected = 0;
nummodes = VID_NumModes();
// DOS does not skip mode 0, because mode 0 is ALWAYS present
i = 0;
for (; i < nummodes && optionsmenu.vidm_nummodes < MAXMODEDESCS; i++)
{
desc = VID_GetModeName(i);
if (desc)
{
vdup = 0;
// when a resolution exists both under VGA and VESA, keep the
// VESA mode, which is always a higher modenum
for (j = 0; j < optionsmenu.vidm_nummodes; j++)
{
if (!strcmp(optionsmenu.modedescs[j].desc, desc))
{
// mode(0): 320x200 is always standard VGA, not vesa
if (optionsmenu.modedescs[j].modenum)
{
optionsmenu.modedescs[j].modenum = i;
vdup = 1;
if (i == vid.modenum)
optionsmenu.vidm_selected = j;
}
else
vdup = 1;
break;
}
}
if (!vdup)
{
optionsmenu.modedescs[optionsmenu.vidm_nummodes].modenum = i;
optionsmenu.modedescs[optionsmenu.vidm_nummodes].desc = desc;
if (i == vid.modenum)
optionsmenu.vidm_selected = optionsmenu.vidm_nummodes;
// Pull out the width and height
sscanf(desc, "%u%*c%u", &width, &height);
// Show multiples of 320x200 as green.
if (SCR_IsAspectCorrect(width, height))
optionsmenu.modedescs[optionsmenu.vidm_nummodes].goodratio = 1;
optionsmenu.vidm_nummodes++;
}
}
}
optionsmenu.vidm_column_size = (optionsmenu.vidm_nummodes+2) / 3;
M_SetupNextMenu(&OPTIONS_VideoModesDef, false);
}
// special menuitem key handler for video mode list
void M_HandleVideoModes(INT32 ch)
{
const UINT8 pid = 0;
(void)ch;
if (optionsmenu.vidm_testingmode > 0)
{
// change back to the previous mode quickly
if (M_MenuBackPressed(pid))
{
setmodeneeded = optionsmenu.vidm_previousmode + 1;
optionsmenu.vidm_testingmode = 0;
}
else if (M_MenuConfirmPressed(pid))
{
S_StartSound(NULL, sfx_s3k5b);
optionsmenu.vidm_testingmode = 0; // stop testing
}
}
else
{
if (menucmd[pid].dpad_ud > 0)
{
S_StartSound(NULL, sfx_s3k5b);
if (++optionsmenu.vidm_selected >= optionsmenu.vidm_nummodes)
optionsmenu.vidm_selected = 0;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
S_StartSound(NULL, sfx_s3k5b);
if (--optionsmenu.vidm_selected < 0)
optionsmenu.vidm_selected = optionsmenu.vidm_nummodes - 1;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr < 0)
{
S_StartSound(NULL, sfx_s3k5b);
optionsmenu.vidm_selected -= optionsmenu.vidm_column_size;
if (optionsmenu.vidm_selected < 0)
optionsmenu.vidm_selected = (optionsmenu.vidm_column_size*3) + optionsmenu.vidm_selected;
if (optionsmenu.vidm_selected >= optionsmenu.vidm_nummodes)
optionsmenu.vidm_selected = optionsmenu.vidm_nummodes - 1;
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr > 0)
{
S_StartSound(NULL, sfx_s3k5b);
optionsmenu.vidm_selected += optionsmenu.vidm_column_size;
if (optionsmenu.vidm_selected >= (optionsmenu.vidm_column_size*3))
optionsmenu.vidm_selected %= optionsmenu.vidm_column_size;
if (optionsmenu.vidm_selected >= optionsmenu.vidm_nummodes)
optionsmenu.vidm_selected = optionsmenu.vidm_nummodes - 1;
M_SetMenuDelay(pid);
}
else if (M_MenuConfirmPressed(pid))
{
M_SetMenuDelay(pid);
S_StartSound(NULL, sfx_s3k5b);
if (vid.modenum == optionsmenu.modedescs[optionsmenu.vidm_selected].modenum)
SCR_SetDefaultMode();
else
{
optionsmenu.vidm_testingmode = 15*TICRATE;
optionsmenu.vidm_previousmode = vid.modenum;
if (!setmodeneeded) // in case the previous setmode was not finished
setmodeneeded = optionsmenu.modedescs[optionsmenu.vidm_selected].modenum + 1;
}
}
else if (M_MenuBackPressed(pid))
{
M_SetMenuDelay(pid);
if (currentMenu->prevMenu)
M_SetupNextMenu(currentMenu->prevMenu, false);
else
M_ClearMenus(true);
}
}
}

17
src/menus/play-1.c Normal file
View file

@ -0,0 +1,17 @@
/// \file menus/play-1.c
/// \brief Play Menu
#include "../k_menu.h"
menuitem_t PLAY_MainMenu[] =
{
{IT_STRING | IT_CALL, "Local Play", "Play only on this computer.",
NULL, {.routine = M_SetupGametypeMenu}, 0, 0},
{IT_STRING | IT_CALL, "Online", "Connect to other computers.",
NULL, {.routine = M_MPOptSelectInit}, /*M_MPRoomSelectInit,*/ 0, 0},
{IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0},
};
menu_t PLAY_MainDef = KARTGAMEMODEMENU(PLAY_MainMenu, &PLAY_CharSelectDef);

1484
src/menus/play-char-select.c Normal file

File diff suppressed because it is too large Load diff

69
src/menus/play-local-1.c Normal file
View file

@ -0,0 +1,69 @@
/// \file menus/play-local-1.c
/// \brief Local Play, gamemode selection menu
#include "../k_menu.h"
#include "../m_cond.h" // Condition Sets
menuitem_t PLAY_GamemodesMenu[] =
{
{IT_STRING | IT_CALL, "Race", "A contest to see who's the fastest of them all!",
NULL, {.routine = M_SetupRaceMenu}, 0, 0},
{IT_STRING | IT_CALL, "Battle", "It's last kart standing in this free-for-all!",
"MENIMG00", {.routine = M_LevelSelectInit}, 0, GT_BATTLE},
{IT_STRING | IT_CALL, "Capsules", "Bust up all of the capsules in record time!",
NULL, {.routine = M_LevelSelectInit}, 1, GT_BATTLE},
{IT_STRING | IT_CALL, "Special", "Strike your target and secure the prize!",
NULL, {.routine = M_LevelSelectInit}, 1, GT_SPECIAL},
{IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0},
};
menu_t PLAY_GamemodesDef = KARTGAMEMODEMENU(PLAY_GamemodesMenu, &PLAY_MainDef);
void M_SetupGametypeMenu(INT32 choice)
{
(void)choice;
PLAY_GamemodesDef.prevMenu = currentMenu;
// Battle and Capsules (and Special) disabled
PLAY_GamemodesMenu[1].status = IT_DISABLED;
PLAY_GamemodesMenu[2].status = IT_DISABLED;
PLAY_GamemodesMenu[3].status = IT_DISABLED;
if (cv_splitplayers.value > 1)
{
// Re-add Battle
PLAY_GamemodesMenu[1].status = IT_STRING | IT_CALL;
}
else
{
boolean anyunlocked = false;
if (M_SecretUnlocked(SECRET_BREAKTHECAPSULES, true))
{
// Re-add Capsules
PLAY_GamemodesMenu[2].status = IT_STRING | IT_CALL;
anyunlocked = true;
}
if (M_SecretUnlocked(SECRET_SPECIALATTACK, true))
{
// Re-add Special
PLAY_GamemodesMenu[3].status = IT_STRING | IT_CALL;
anyunlocked = true;
}
if (!anyunlocked)
{
// Only one non-Back entry, let's skip straight to Race.
M_SetupRaceMenu(-1);
return;
}
}
M_SetupNextMenu(&PLAY_GamemodesDef, false);
}

View file

@ -0,0 +1,40 @@
/// \file menus/play-local-race-1.c
/// \brief Race Mode Menu
#include "../k_menu.h"
#include "../m_cond.h" // Condition Sets
menuitem_t PLAY_RaceGamemodesMenu[] =
{
{IT_STRING | IT_CALL, "Grand Prix", "Compete for the best rank over five races!",
NULL, {.routine = M_SetupDifficultySelect}, 0, 0},
{IT_STRING | IT_CALL, "Match Race", "Play by your own rules in a specialized, single race!",
"MENIMG01", {.routine = M_SetupDifficultySelect}, 1, 0},
{IT_STRING | IT_CALL, "Time Attack", "Record your best time on any track!",
NULL, {.routine = M_LevelSelectInit}, 1, GT_RACE},
{IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0},
};
menu_t PLAY_RaceGamemodesDef = KARTGAMEMODEMENU(PLAY_RaceGamemodesMenu, &PLAY_GamemodesDef);
void M_SetupRaceMenu(INT32 choice)
{
(void)choice;
PLAY_RaceGamemodesDef.prevMenu = currentMenu;
// Time Attack disabled
PLAY_RaceGamemodesMenu[2].status = IT_DISABLED;
// Time Attack is 1P only
if (cv_splitplayers.value <= 1
&& M_SecretUnlocked(SECRET_TIMEATTACK, true))
{
PLAY_RaceGamemodesMenu[2].status = IT_STRING | IT_CALL;
}
M_SetupNextMenu(&PLAY_RaceGamemodesDef, false);
}

View file

@ -0,0 +1,110 @@
/// \file menus/play-local-race-difficulty.c
/// \brief difficulty selection -- see drace_e
#include "../k_menu.h"
#include "../m_cond.h" // Condition Sets
menuitem_t PLAY_RaceDifficulty[] =
{
// For GP
{IT_STRING | IT_CVAR, "Difficulty", "Select the game difficulty",
NULL, {.cvar = &cv_dummygpdifficulty}, 0, 0},
// Match Race
{IT_STRING | IT_CVAR, "Difficulty", "Select the game speed",
NULL, {.cvar = &cv_dummykartspeed}, 0, 0},
// DISABLE THAT OPTION OUTSIDE OF MATCH RACE
{IT_STRING2 | IT_CVAR, "CPU", "Set the difficulty of CPU players.",
NULL, {.cvar = &cv_dummymatchbots}, 0, 0},
{IT_STRING2 | IT_CVAR, "Racers", "Sets the number of racers, including players and CPU.",
NULL, {.cvar = &cv_maxplayers}, 0, 0},
{IT_STRING2 | IT_CVAR, "Encore", "Enable or disable Encore mode",
NULL, {.cvar = &cv_dummygpencore}, 0, 0},
// For GP
{IT_STRING | IT_CALL, "Cup Select", "Go on and select a cup!", NULL, {.routine = M_LevelSelectInit}, 2, GT_RACE},
// Match Race
{IT_STRING | IT_CALL, "Map Select", "Go on and select a race track!", NULL, {.routine = M_LevelSelectInit}, 0, GT_RACE},
{IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0},
};
menu_t PLAY_RaceDifficultyDef = {
sizeof(PLAY_RaceDifficulty) / sizeof(menuitem_t),
&PLAY_RaceGamemodesDef,
0,
PLAY_RaceDifficulty,
0, 0,
0, 0,
1, 5,
M_DrawRaceDifficulty,
NULL,
NULL,
NULL,
NULL
};
consvar_t cv_dummygpdifficulty = CVAR_INIT ("dummygpdifficulty", "Normal", CV_HIDDEN, gpdifficulty_cons_t, NULL);
consvar_t cv_dummykartspeed = CVAR_INIT ("dummykartspeed", "Normal", CV_HIDDEN, dummykartspeed_cons_t, NULL);
consvar_t cv_dummygpencore = CVAR_INIT ("dummygpencore", "Off", CV_HIDDEN, CV_OnOff, NULL);
static CV_PossibleValue_t dummymatchbots_cons_t[] = {
{0, "Off"},
{1, "Lv.1"},
{2, "Lv.2"},
{3, "Lv.3"},
{4, "Lv.4"},
{5, "Lv.5"},
{6, "Lv.6"},
{7, "Lv.7"},
{8, "Lv.8"},
{9, "Lv.9"},
{10, "Lv.10"},
{11, "Lv.11"},
{12, "Lv.12"},
{13, "Lv.MAX"},
{0, NULL}
};
consvar_t cv_dummymatchbots = CVAR_INIT ("dummymatchbots", "Off", CV_HIDDEN, dummymatchbots_cons_t, NULL);
void M_SetupDifficultySelect(INT32 choice)
{
// check what we picked.
choice = currentMenu->menuitems[itemOn].mvar1;
// setup the difficulty menu and then remove choices depending on choice
PLAY_RaceDifficultyDef.prevMenu = currentMenu;
PLAY_RaceDifficulty[drace_gpdifficulty].status = IT_DISABLED;
PLAY_RaceDifficulty[drace_mrkartspeed].status = IT_DISABLED;
PLAY_RaceDifficulty[drace_mrcpu].status = IT_DISABLED;
PLAY_RaceDifficulty[drace_mrracers].status = IT_DISABLED;
PLAY_RaceDifficulty[drace_encore].status = IT_DISABLED;
PLAY_RaceDifficulty[drace_cupselect].status = IT_DISABLED;
PLAY_RaceDifficulty[drace_mapselect].status = IT_DISABLED;
if (choice) // Match Race
{
PLAY_RaceDifficulty[drace_mrkartspeed].status = IT_STRING|IT_CVAR; // Kart Speed
PLAY_RaceDifficulty[drace_mrcpu].status = IT_STRING2|IT_CVAR; // CPUs on/off
PLAY_RaceDifficulty[drace_mrracers].status = IT_STRING2|IT_CVAR; // CPU amount
PLAY_RaceDifficulty[drace_mapselect].status = IT_STRING|IT_CALL; // Level Select (Match Race)
PLAY_RaceDifficultyDef.lastOn = drace_mapselect; // Select map select by default.
}
else // GP
{
PLAY_RaceDifficulty[drace_gpdifficulty].status = IT_STRING|IT_CVAR; // Difficulty
PLAY_RaceDifficulty[drace_cupselect].status = IT_STRING|IT_CALL; // Level Select (GP)
PLAY_RaceDifficultyDef.lastOn = drace_cupselect; // Select cup select by default.
}
if (M_SecretUnlocked(SECRET_ENCORE, false))
{
PLAY_RaceDifficulty[drace_encore].status = IT_STRING2|IT_CVAR; // Encore on/off
}
M_SetupNextMenu(&PLAY_RaceDifficultyDef, false);
}

View file

@ -0,0 +1,210 @@
/// \file menus/play-local-race-time-attack.c
/// \brief Race Time Attack Menu
#include "../k_menu.h"
#include "../r_local.h" // SplitScreen_OnChange
#include "../s_sound.h"
#include "../f_finale.h" // F_WipeStartScreen
#include "../v_video.h"
#include "../d_main.h" // srb2home
#include "../m_misc.h" // M_MkdirEach
// see ta_e
menuitem_t PLAY_TimeAttack[] =
{
{IT_STRING | IT_SUBMENU, "Replay...", NULL, NULL, {.submenu = &PLAY_TAReplayDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Guest...", NULL, NULL, {.submenu = &PLAY_TAReplayGuestDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Ghosts...", NULL, NULL, {.submenu = &PLAY_TAGhostsDef}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Start", NULL, NULL, {.routine = M_StartTimeAttack}, 0, 0},
};
menu_t PLAY_TimeAttackDef = {
sizeof(PLAY_TimeAttack) / sizeof(menuitem_t),
&PLAY_LevelSelectDef,
0,
PLAY_TimeAttack,
0, 0,
0, 0,
2, 5,
M_DrawTimeAttack,
NULL,
NULL,
NULL,
NULL
};
menuitem_t PLAY_TAReplay[] =
{
{IT_STRING | IT_CALL, "Replay Best Time", NULL, NULL, {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_STRING | IT_CALL, "Replay Best Lap", NULL, NULL, {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Replay Last", NULL, NULL, {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_STRING | IT_CALL, "Replay Guest", NULL, NULL, {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_STRING | IT_CALL, "Replay Staff", NULL, NULL, {.routine = M_HandleStaffReplay}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Back", NULL, NULL, {.submenu = &PLAY_TimeAttackDef}, 0, 0},
};
menu_t PLAY_TAReplayDef = {
sizeof(PLAY_TAReplay) / sizeof(menuitem_t),
&PLAY_TimeAttackDef,
0,
PLAY_TAReplay,
0, 0,
0, 0,
2, 5,
M_DrawTimeAttack,
NULL,
NULL,
NULL,
NULL
};
menuitem_t PLAY_TAReplayGuest[] =
{
{IT_HEADERTEXT|IT_HEADER, "Save as guest...", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Best Time", NULL, NULL, {.routine = M_SetGuestReplay}, 0, 0},
{IT_STRING | IT_CALL, "Best Lap", NULL, NULL, {.routine = M_SetGuestReplay}, 0, 0},
{IT_STRING | IT_CALL, "Last Run", NULL, NULL, {.routine = M_SetGuestReplay}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Delete Guest", NULL, NULL, {.routine = M_SetGuestReplay}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Back", NULL, NULL, {.submenu = &PLAY_TimeAttackDef}, 0, 0},
};
menu_t PLAY_TAReplayGuestDef = {
sizeof(PLAY_TAReplayGuest) / sizeof(menuitem_t),
&PLAY_TimeAttackDef,
0,
PLAY_TAReplayGuest,
0, 0,
0, 0,
2, 5,
M_DrawTimeAttack,
NULL,
NULL,
NULL,
NULL
};
menuitem_t PLAY_TAGhosts[] =
{
{IT_STRING | IT_CVAR, "Best Time", NULL, NULL, {.cvar = &cv_ghost_besttime}, 0, 0},
{IT_STRING | IT_CVAR, "Best Lap", NULL, NULL, {.cvar = &cv_ghost_bestlap}, 0, 0},
{IT_STRING | IT_CVAR, "Last", NULL, NULL, {.cvar = &cv_ghost_last}, 0, 0},
{IT_DISABLED, "Guest", NULL, NULL, {.cvar = &cv_ghost_guest}, 0, 0},
{IT_DISABLED, "Staff", NULL, NULL, {.cvar = &cv_ghost_staff}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Back", NULL, NULL, {.submenu = &PLAY_TimeAttackDef}, 0, 0},
};
menu_t PLAY_TAGhostsDef = {
sizeof(PLAY_TAGhosts) / sizeof(menuitem_t),
&PLAY_TimeAttackDef,
0,
PLAY_TAGhosts,
0, 0,
0, 0,
2, 5,
M_DrawTimeAttack,
NULL,
NULL,
NULL,
NULL
};
// autorecord demos for time attack
consvar_t cv_autorecord = CVAR_INIT ("autorecord", "Yes", 0, CV_YesNo, NULL);
CV_PossibleValue_t ghost_cons_t[] = {{0, "Hide"}, {1, "Show Character"}, {2, "Show All"}, {0, NULL}};
CV_PossibleValue_t ghost2_cons_t[] = {{0, "Hide"}, {1, "Show"}, {0, NULL}};
consvar_t cv_ghost_besttime = CVAR_INIT ("ghost_besttime", "Show All", CV_SAVE, ghost_cons_t, NULL);
consvar_t cv_ghost_bestlap = CVAR_INIT ("ghost_bestlap", "Show All", CV_SAVE, ghost_cons_t, NULL);
consvar_t cv_ghost_last = CVAR_INIT ("ghost_last", "Show All", CV_SAVE, ghost_cons_t, NULL);
consvar_t cv_ghost_guest = CVAR_INIT ("ghost_guest", "Show", CV_SAVE, ghost2_cons_t, NULL);
consvar_t cv_ghost_staff = CVAR_INIT ("ghost_staff", "Show", CV_SAVE, ghost2_cons_t, NULL);
// time attack stuff...
void M_HandleStaffReplay(INT32 choice)
{
// @TODO:
(void) choice;
}
void M_ReplayTimeAttack(INT32 choice)
{
// @TODO:
(void) choice;
}
void M_SetGuestReplay(INT32 choice)
{
// @TODO:
(void) choice;
}
void M_StartTimeAttack(INT32 choice)
{
char *gpath;
char nameofdemo[256];
(void)choice;
modeattacking = ATTACKING_TIME;
if ((gametypes[levellist.newgametype]->rules & GTR_CIRCUIT)
&& (mapheaderinfo[levellist.choosemap]->numlaps != 1))
{
modeattacking |= ATTACKING_LAP;
}
// Still need to reset devmode
cht_debug = 0;
emeralds = 0;
if (demo.playback)
G_StopDemo();
if (metalrecording)
G_StopMetalDemo();
splitscreen = 0;
SplitScreen_OnChange();
S_StartSound(NULL, sfx_s3k63);
paused = false;
// Early fadeout to let the sound finish playing
F_WipeStartScreen();
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
F_WipeEndScreen();
F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false);
SV_StartSinglePlayerServer(levellist.newgametype, false);
gpath = va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s",
srb2home, timeattackfolder);
M_MkdirEach(gpath, M_PathParts(gpath) - 3, 0755);
strcat(gpath, PATHSEP);
strcat(gpath, G_BuildMapName(levellist.choosemap+1));
snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-last", gpath, cv_skin[0].string);
if (!cv_autorecord.value)
remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
else
G_RecordDemo(nameofdemo);
M_ClearMenus(true);
D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_dummygpencore.value == 1), 1, 1, false, false);
}

84
src/menus/play-online-1.c Normal file
View file

@ -0,0 +1,84 @@
/// \file menus/play-online-1.c
/// \brief MULTIPLAYER OPTION SELECT
#include "../k_menu.h"
menuitem_t PLAY_MP_OptSelect[] =
{
//{IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, M_MPOptSelect, 0, 0},
{IT_STRING | IT_CALL, "Host Game", "Start your own online game!",
NULL, {.routine = M_MPHostInit}, 0, 0},
{IT_STRING | IT_CALL, "Server Browser", "Search for game servers to play in.",
NULL, {.routine = M_MPRoomSelectInit}, 0, 0},
{IT_STRING | IT_CALL, "Join by IP", "Join an online game by its IP address.",
NULL, {.routine = M_MPJoinIPInit}, 0, 0},
};
menu_t PLAY_MP_OptSelectDef = {
sizeof (PLAY_MP_OptSelect) / sizeof (menuitem_t),
&PLAY_MainDef,
0,
PLAY_MP_OptSelect,
0, 0,
0, 0,
-1, 1,
M_DrawMPOptSelect,
M_MPOptSelectTick,
NULL,
NULL,
NULL
};
struct mpmenu_s mpmenu;
// Use this as a quit routine within the HOST GAME and JOIN BY IP "sub" menus
boolean M_MPResetOpts(void)
{
UINT8 i = 0;
for (; i < 3; i++)
mpmenu.modewinextend[i][0] = 0; // Undo this
return true;
}
void M_MPOptSelectInit(INT32 choice)
{
INT16 arrcpy[3][3] = {{0,68,0}, {0,12,0}, {0,74,0}};
UINT8 i = 0, j = 0; // To copy the array into the struct
const UINT32 forbidden = GTR_FORBIDMP;
(void)choice;
mpmenu.modechoice = 0;
mpmenu.ticker = 0;
for (; i < 3; i++)
for (j = 0; j < 3; j++)
mpmenu.modewinextend[i][j] = arrcpy[i][j]; // I miss Lua already
// Guarantee menugametype is good
M_NextMenuGametype(forbidden);
M_PrevMenuGametype(forbidden);
M_SetupNextMenu(&PLAY_MP_OptSelectDef, false);
}
void M_MPOptSelectTick(void)
{
UINT8 i = 0;
// 3 Because we have 3 options in the menu
for (; i < 3; i++)
{
if (mpmenu.modewinextend[i][0])
mpmenu.modewinextend[i][2] += 8;
else
mpmenu.modewinextend[i][2] -= 8;
mpmenu.modewinextend[i][2] = min(mpmenu.modewinextend[i][1], max(0, mpmenu.modewinextend[i][2]));
//CONS_Printf("%d - %d,%d,%d\n", i, mpmenu.modewinextend[i][0], mpmenu.modewinextend[i][1], mpmenu.modewinextend[i][2]);
}
}

View file

@ -0,0 +1,112 @@
/// \file menus/play-online-host.c
/// \brief MULTIPLAYER HOST SCREEN -- see mhost_e
#include "../k_menu.h"
#include "../s_sound.h"
// MULTIPLAYER HOST SCREEN -- see mhost_e
menuitem_t PLAY_MP_Host[] =
{
//{IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, M_MPOptSelect, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_STRING, "Server Name", "Display name for your game online. Other players will see this.",
NULL, {.cvar = &cv_servername}, 0, 0},
{IT_STRING | IT_CVAR, "Public Server", "Display or not your game in the Server Browser for other players.",
NULL, {.cvar = &cv_advertise}, 0, 0},
{IT_STRING | IT_CVAR, "Max. Players", "Set how many players can play at once. Others will spectate.",
NULL, {.cvar = &cv_maxplayers}, 0, 0},
{IT_STRING | IT_KEYHANDLER, "Gamemode", "Choose the type of play on your server.",
NULL, {.routine = M_HandleHostMenuGametype}, 0, 0},
{IT_STRING | IT_CALL, "GO", "Select a map with the currently selected gamemode",
NULL, {.routine = M_MPSetupNetgameMapSelect}, 0, 0},
};
menu_t PLAY_MP_HostDef = {
sizeof (PLAY_MP_Host) / sizeof (menuitem_t),
&PLAY_MP_OptSelectDef,
0,
PLAY_MP_Host,
0, 0,
0, 0,
-1, 1, // 1 frame transition.... This is really just because I don't want the black fade when we press esc, hehe
M_DrawMPHost,
M_MPOptSelectTick, // This handles the unfolding options
NULL,
M_MPResetOpts,
NULL
};
void M_MPHostInit(INT32 choice)
{
(void)choice;
mpmenu.modewinextend[0][0] = 1;
M_SetupNextMenu(&PLAY_MP_HostDef, true);
itemOn = mhost_go;
}
void M_HandleHostMenuGametype(INT32 choice)
{
const UINT8 pid = 0;
const UINT32 forbidden = GTR_FORBIDMP;
(void)choice;
if (M_MenuBackPressed(pid))
{
M_GoBack(0);
M_SetMenuDelay(pid);
return;
}
else if (menucmd[pid].dpad_lr > 0 || M_MenuConfirmPressed(pid))
{
M_NextMenuGametype(forbidden);
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr < 0)
{
M_PrevMenuGametype(forbidden);
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
if (menucmd[pid].dpad_ud > 0)
{
M_NextOpt();
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
M_PrevOpt();
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
}
void M_MPSetupNetgameMapSelect(INT32 choice)
{
(void)choice;
// Yep, we'll be starting a netgame.
levellist.netgame = true;
// Make sure we reset those
levellist.levelsearch.timeattack = false;
levellist.levelsearch.checklocked = true;
cupgrid.grandprix = false;
// okay this is REALLY stupid but this fixes the host menu re-folding on itself when we go back.
mpmenu.modewinextend[0][0] = 1;
if (!M_LevelListFromGametype(menugametype))
{
S_StartSound(NULL, sfx_s3kb2);
M_StartMessage(va("No levels available for\n%s Mode!\n\nPress (B)\n", gametypes[menugametype]->name), NULL, MM_NOTHING);
}
}

View file

@ -0,0 +1,110 @@
/// \file menus/play-online-join-ip.c
/// \brief MULTIPLAYER JOIN BY IP
#include "../k_menu.h"
#include "../v_video.h"
#include "../i_system.h" // I_OsPolling
#include "../i_video.h" // I_UpdateNoBlit
#include "../m_misc.h" // NUMLOGIP
menuitem_t PLAY_MP_JoinIP[] =
{
//{IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, M_MPOptSelect, 0, 0},
{IT_STRING | IT_CVAR | IT_CV_STRING, "IP: ", "Type the IPv4 address of the server.",
NULL, {.cvar = &cv_dummyip}, 0, 0},
{IT_STRING, "CONNECT ", "Attempt to connect to the server you entered the IP for.",
NULL, {NULL}, 0, 0},
{IT_STRING | IT_SPACE, "LAST IPs JOINED:", "Kanade best waifu :)",
NULL, {NULL}, 0, 0},
{IT_STRING, "servip1", "The last 3 IPs you've succesfully joined are displayed here.",
NULL, {NULL}, 0, 0},
{IT_STRING, "servip2", "The last 3 IPs you've succesfully joined are displayed here.",
NULL, {NULL}, 0, 0},
{IT_STRING, "servip3", "The last 3 IPs you've succesfully joined are displayed here.",
NULL, {NULL}, 0, 0},
};
menu_t PLAY_MP_JoinIPDef = {
sizeof (PLAY_MP_JoinIP) / sizeof (menuitem_t),
&PLAY_MP_OptSelectDef,
0,
PLAY_MP_JoinIP,
0, 0,
0, 0,
-1, 1, // 1 frame transition.... This is really just because I don't want the black fade when we press esc, hehe
M_DrawMPJoinIP,
M_MPOptSelectTick, // This handles the unfolding options
NULL,
M_MPResetOpts,
M_JoinIPInputs
};
consvar_t cv_dummyip = CVAR_INIT ("dummyip", "", CV_HIDDEN, NULL, NULL);
void M_MPJoinIPInit(INT32 choice)
{
(void)choice;
mpmenu.modewinextend[2][0] = 1;
M_SetupNextMenu(&PLAY_MP_JoinIPDef, true);
}
// Attempts to join a given IP from the menu.
void M_JoinIP(const char *ipa)
{
if (*(ipa) == '\0') // Jack shit
{
M_StartMessage("Please specify an address.\n", NULL, MM_NOTHING);
return;
}
COM_BufAddText(va("connect \"%s\"\n", ipa));
// A little "please wait" message.
M_DrawTextBox(56, BASEVIDHEIGHT/2-12, 24, 2);
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Connecting to server...");
I_OsPolling();
I_UpdateNoBlit();
if (rendermode == render_soft)
I_FinishUpdate(); // page flip or blit buffer
}
boolean M_JoinIPInputs(INT32 ch)
{
const UINT8 pid = 0;
(void) ch;
if (itemOn == 1) // connect field
{
// enter: connect
if (M_MenuConfirmPressed(pid))
{
M_JoinIP(cv_dummyip.string);
M_SetMenuDelay(pid);
return true;
}
}
else if (currentMenu->numitems - itemOn <= NUMLOGIP && M_MenuConfirmPressed(pid)) // On one of the last 3 options for IP rejoining
{
UINT8 index = NUMLOGIP - (currentMenu->numitems - itemOn);
M_SetMenuDelay(pid);
// Is there an address at this part of the table?
if (*joinedIPlist[index][0])
M_JoinIP(joinedIPlist[index][0]);
else
S_StartSound(NULL, sfx_lose);
return true; // eat input.
}
return false;
}

View file

@ -0,0 +1,66 @@
/// \file menus/play-online-room-select.c
/// \brief MULTIPLAYER ROOM SELECT MENU
#include "../k_menu.h"
#include "../s_sound.h"
menuitem_t PLAY_MP_RoomSelect[] =
{
{IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, {.routine = M_MPRoomSelect}, 0, 0},
};
menu_t PLAY_MP_RoomSelectDef = {
sizeof (PLAY_MP_RoomSelect) / sizeof (menuitem_t),
&PLAY_MP_OptSelectDef,
0,
PLAY_MP_RoomSelect,
0, 0,
0, 0,
0, 0,
M_DrawMPRoomSelect,
M_MPRoomSelectTick,
NULL,
NULL,
NULL
};
void M_MPRoomSelect(INT32 choice)
{
const UINT8 pid = 0;
(void) choice;
if (menucmd[pid].dpad_lr)
{
mpmenu.room = (!mpmenu.room) ? 1 : 0;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (M_MenuBackPressed(pid))
{
M_GoBack(0);
M_SetMenuDelay(pid);
}
else if (M_MenuConfirmPressed(pid))
{
M_ServersMenu(0);
M_SetMenuDelay(pid);
}
}
void M_MPRoomSelectTick(void)
{
mpmenu.ticker++;
}
void M_MPRoomSelectInit(INT32 choice)
{
(void)choice;
mpmenu.room = 0;
mpmenu.ticker = 0;
mpmenu.servernum = 0;
mpmenu.scrolln = 0;
mpmenu.slide = 0;
M_SetupNextMenu(&PLAY_MP_RoomSelectDef, false);
}

View file

@ -0,0 +1,494 @@
/// \file menus/play-online-server-browser.c
/// \brief MULTIPLAYER ROOM FETCH / REFRESH THREADS
#include "../k_menu.h"
#include "../v_video.h"
#include "../i_system.h" // I_OsPolling
#include "../i_video.h" // I_UpdateNoBlit
#ifdef SERVERLISTDEBUG
#include "../m_random.h"
#endif
menuitem_t PLAY_MP_ServerBrowser[] =
{
{IT_STRING | IT_CVAR, "SORT BY", NULL, // tooltip MUST be null.
NULL, {.cvar = &cv_serversort}, 0, 0},
{IT_STRING, "REFRESH", NULL,
NULL, {NULL}, 0, 0},
{IT_NOTHING, NULL, NULL, NULL, {NULL}, 0, 0},
};
menu_t PLAY_MP_ServerBrowserDef = {
sizeof (PLAY_MP_ServerBrowser) / sizeof (menuitem_t),
&PLAY_MP_RoomSelectDef,
0,
PLAY_MP_ServerBrowser,
32, 36,
0, 0,
0, 0,
M_DrawMPServerBrowser,
M_MPServerBrowserTick,
NULL,
NULL,
M_ServerBrowserInputs
};
static CV_PossibleValue_t serversort_cons_t[] = {
{0,"Ping"},
{1,"AVG. Power Level"},
{2,"Most Players"},
{3,"Least Players"},
{4,"Max Player Slots"},
{5,"Gametype"},
{0,NULL}
};
consvar_t cv_serversort = CVAR_INIT ("serversort", "Ping", CV_CALL, serversort_cons_t, M_SortServerList);
// for server fetch threads...
M_waiting_mode_t m_waiting_mode = M_NOT_WAITING;
// depending on mpmenu.room, either allows only unmodded servers or modded ones. Remove others from the list.
// we do this by iterating backwards.
static void M_CleanServerList(void)
{
UINT8 i = serverlistcount;
while (i)
{
if (serverlist[i].info.modifiedgame != mpmenu.room)
{
// move everything after this index 1 slot down...
if (i != serverlistcount)
memcpy(&serverlist[i], &serverlist[i+1], sizeof(serverelem_t)*(serverlistcount-i));
serverlistcount--;
}
i--;
}
}
void
M_SetWaitingMode (int mode)
{
#ifdef HAVE_THREADS
I_lock_mutex(&k_menu_mutex);
#endif
{
m_waiting_mode = mode;
}
#ifdef HAVE_THREADS
I_unlock_mutex(k_menu_mutex);
#endif
}
int
M_GetWaitingMode (void)
{
int mode;
#ifdef HAVE_THREADS
I_lock_mutex(&k_menu_mutex);
#endif
{
mode = m_waiting_mode;
}
#ifdef HAVE_THREADS
I_unlock_mutex(k_menu_mutex);
#endif
return mode;
}
#ifdef MASTERSERVER
#ifdef HAVE_THREADS
void
Spawn_masterserver_thread (const char *name, void (*thread)(int*))
{
int *id = malloc(sizeof *id);
I_lock_mutex(&ms_QueryId_mutex);
{
*id = ms_QueryId;
}
I_unlock_mutex(ms_QueryId_mutex);
I_spawn_thread(name, (I_thread_fn)thread, id);
}
int
Same_instance (int id)
{
int okay;
I_lock_mutex(&ms_QueryId_mutex);
{
okay = ( id == ms_QueryId );
}
I_unlock_mutex(ms_QueryId_mutex);
return okay;
}
#endif/*HAVE_THREADS*/
void
Fetch_servers_thread (int *id)
{
msg_server_t * server_list;
(void)id;
M_SetWaitingMode(M_WAITING_SERVERS);
#ifdef HAVE_THREADS
server_list = GetShortServersList(*id);
#else
server_list = GetShortServersList(0);
#endif
if (server_list)
{
#ifdef HAVE_THREADS
if (Same_instance(*id))
#endif
{
M_SetWaitingMode(M_NOT_WAITING);
#ifdef HAVE_THREADS
I_lock_mutex(&ms_ServerList_mutex);
{
ms_ServerList = server_list;
}
I_unlock_mutex(ms_ServerList_mutex);
#else
CL_QueryServerList(server_list);
free(server_list);
#endif
}
#ifdef HAVE_THREADS
else
{
free(server_list);
}
#endif
}
#ifdef HAVE_THREADS
free(id);
#endif
}
#endif/*MASTERSERVER*/
// updates serverlist
void M_RefreshServers(INT32 choice)
{
(void)choice;
// Display a little "please wait" message.
M_DrawTextBox(52, BASEVIDHEIGHT/2-10, 25, 3);
V_DrawCenteredString(BASEVIDWIDTH/2, BASEVIDHEIGHT/2, 0, "Searching for servers...");
V_DrawCenteredString(BASEVIDWIDTH/2, (BASEVIDHEIGHT/2)+12, 0, "Please wait.");
I_OsPolling();
I_UpdateNoBlit();
if (rendermode == render_soft)
I_FinishUpdate(); // page flip or blit buffer
#ifdef MASTERSERVER
#ifdef HAVE_THREADS
Spawn_masterserver_thread("fetch-servers", Fetch_servers_thread);
#else/*HAVE_THREADS*/
Fetch_servers_thread(NULL);
#endif/*HAVE_THREADS*/
#else/*MASTERSERVER*/
CL_UpdateServerList();
#endif/*MASTERSERVER*/
#ifdef SERVERLISTDEBUG
M_ServerListFillDebug();
#endif
M_CleanServerList();
M_SortServerList();
}
#ifdef UPDATE_ALERT
static void M_CheckMODVersion(int id)
{
char updatestring[500];
const char *updatecheck = GetMODVersion(id);
if(updatecheck)
{
sprintf(updatestring, UPDATE_ALERT_STRING, VERSIONSTRING, updatecheck);
#ifdef HAVE_THREADS
I_lock_mutex(&k_menu_mutex);
#endif
M_StartMessage(updatestring, NULL, MM_NOTHING);
#ifdef HAVE_THREADS
I_unlock_mutex(k_menu_mutex);
#endif
}
}
#endif/*UPDATE_ALERT*/
#if defined (UPDATE_ALERT) && defined (HAVE_THREADS)
static void
Check_new_version_thread (int *id)
{
M_SetWaitingMode(M_WAITING_VERSION);
M_CheckMODVersion(*id);
if (Same_instance(*id))
{
Fetch_servers_thread(id);
}
else
{
free(id);
}
}
#endif/*defined (UPDATE_ALERT) && defined (HAVE_THREADS)*/
// Initializes serverlist when entering the menu...
void M_ServersMenu(INT32 choice)
{
(void)choice;
// modified game check: no longer handled
// we don't request a restart unless the filelist differs
mpmenu.servernum = 0;
mpmenu.scrolln = 0;
mpmenu.slide = 0;
M_SetupNextMenu(&PLAY_MP_ServerBrowserDef, false);
itemOn = 0;
#if defined (MASTERSERVER) && defined (HAVE_THREADS)
I_lock_mutex(&ms_QueryId_mutex);
{
ms_QueryId++;
}
I_unlock_mutex(ms_QueryId_mutex);
I_lock_mutex(&ms_ServerList_mutex);
{
if (ms_ServerList)
{
free(ms_ServerList);
ms_ServerList = NULL;
}
}
I_unlock_mutex(ms_ServerList_mutex);
#ifdef UPDATE_ALERT
Spawn_masterserver_thread("check-new-version", Check_new_version_thread);
#else/*UPDATE_ALERT*/
Spawn_masterserver_thread("fetch-servers", Fetch_servers_thread);
#endif/*UPDATE_ALERT*/
#else/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
#ifdef UPDATE_ALERT
M_CheckMODVersion(0);
#endif/*UPDATE_ALERT*/
M_RefreshServers(0);
#endif/*defined (MASTERSERVER) && defined (HAVE_THREADS)*/
#ifdef SERVERLISTDEBUG
M_ServerListFillDebug();
#endif
M_CleanServerList();
M_SortServerList();
}
#ifdef SERVERLISTDEBUG
// Fill serverlist with a bunch of garbage to make our life easier in debugging
void M_ServerListFillDebug(void)
{
UINT8 i = 0;
serverlistcount = 10;
memset(serverlist, 0, sizeof(serverlist)); // zero out the array for convenience...
for (i = 0; i < serverlistcount; i++)
{
// We don't really care about the server node for this, let's just fill in the info so that we have a visual...
serverlist[i].info.numberofplayer = min(i, 8);
serverlist[i].info.maxplayer = 8;
serverlist[i].info.avgpwrlv = P_RandomRange(PR_UNDEFINED, 500, 1500);
serverlist[i].info.time = P_RandomRange(PR_UNDEFINED, 1, 8); // ping
strcpy(serverlist[i].info.servername, va("Serv %d", i+1));
strcpy(serverlist[i].info.gametypename, i & 1 ? "Race" : "Battle");
P_RandomRange(PR_UNDEFINED, 0, 5); // change results...
serverlist[i].info.kartvars = P_RandomRange(PR_UNDEFINED, 0, 3) & SV_SPEEDMASK;
serverlist[i].info.modifiedgame = P_RandomRange(PR_UNDEFINED, 0, 1);
CONS_Printf("Serv %d | %d...\n", i, serverlist[i].info.modifiedgame);
}
}
#endif // SERVERLISTDEBUG
// Ascending order, not descending.
// The casts are safe as long as the caller doesn't do anything stupid.
#define SERVER_LIST_ENTRY_COMPARATOR(key) \
static int ServerListEntryComparator_##key(const void *entry1, const void *entry2) \
{ \
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \
if (sa->info.key != sb->info.key) \
return sa->info.key - sb->info.key; \
return strcmp(sa->info.servername, sb->info.servername); \
}
// This does descending instead of ascending.
#define SERVER_LIST_ENTRY_COMPARATOR_REVERSE(key) \
static int ServerListEntryComparator_##key##_reverse(const void *entry1, const void *entry2) \
{ \
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2; \
if (sb->info.key != sa->info.key) \
return sb->info.key - sa->info.key; \
return strcmp(sb->info.servername, sa->info.servername); \
}
SERVER_LIST_ENTRY_COMPARATOR(time)
SERVER_LIST_ENTRY_COMPARATOR(numberofplayer)
SERVER_LIST_ENTRY_COMPARATOR_REVERSE(numberofplayer)
SERVER_LIST_ENTRY_COMPARATOR_REVERSE(maxplayer)
SERVER_LIST_ENTRY_COMPARATOR(avgpwrlv)
static int ServerListEntryComparator_gametypename(const void *entry1, const void *entry2)
{
const serverelem_t *sa = (const serverelem_t*)entry1, *sb = (const serverelem_t*)entry2;
int c;
if (( c = strcasecmp(sa->info.gametypename, sb->info.gametypename) ))
return c;
return strcmp(sa->info.servername, sb->info.servername); \
}
void M_SortServerList(void)
{
switch(cv_serversort.value)
{
case 0: // Ping.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_time);
break;
case 1: // AVG. Power Level
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_avgpwrlv);
break;
case 2: // Most players.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer_reverse);
break;
case 3: // Least players.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_numberofplayer);
break;
case 4: // Max players.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_maxplayer_reverse);
break;
case 5: // Gametype.
qsort(serverlist, serverlistcount, sizeof(serverelem_t), ServerListEntryComparator_gametypename);
break;
}
}
// Server browser inputs & ticker
void M_MPServerBrowserTick(void)
{
mpmenu.slide /= 2;
}
// Input handler for server browser.
boolean M_ServerBrowserInputs(INT32 ch)
{
UINT8 pid = 0;
UINT8 maxscroll = serverlistcount-(SERVERSPERPAGE/2);
(void) ch;
if (!itemOn && menucmd[pid].dpad_ud < 0)
{
M_PrevOpt(); // go to itemOn 2
if (serverlistcount)
{
UINT8 prevscroll = mpmenu.scrolln;
mpmenu.servernum = serverlistcount;
mpmenu.scrolln = maxscroll;
mpmenu.slide = SERVERSPACE * (prevscroll - mpmenu.scrolln);
}
else
{
itemOn = 1; // Sike! If there are no servers, go to refresh instead.
}
return true; // overwrite behaviour.
}
else if (itemOn == 2) // server browser itself...
{
// we have to manually do that here.
if (M_MenuBackPressed(pid))
{
M_GoBack(0);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud > 0) // down
{
if (mpmenu.servernum >= serverlistcount-1)
{
UINT8 prevscroll = mpmenu.scrolln;
mpmenu.servernum = 0;
mpmenu.scrolln = 0;
mpmenu.slide = SERVERSPACE * (prevscroll - mpmenu.scrolln);
M_NextOpt(); // Go back to the top of the real menu.
}
else
{
mpmenu.servernum++;
if (mpmenu.scrolln < maxscroll && mpmenu.servernum > SERVERSPERPAGE/2)
{
mpmenu.scrolln++;
mpmenu.slide += SERVERSPACE;
}
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
if (!mpmenu.servernum)
{
M_PrevOpt();
}
else
{
if (mpmenu.servernum <= serverlistcount-(SERVERSPERPAGE/2) && mpmenu.scrolln)
{
mpmenu.scrolln--;
mpmenu.slide -= SERVERSPACE;
}
mpmenu.servernum--;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
return true; // Overwrite behaviour.
}
return false; // use normal behaviour.
}

View file

@ -0,0 +1,11 @@
target_sources(SRB2SDL2 PRIVATE
cup-select.c
explosions.c
level-select.c
gametype.c
manual.c
message-box.c
pause-game.c
pause-replay.c
virtual-keyboard.c
)

View file

@ -0,0 +1,195 @@
/// \file menus/transient/cup-select.c
/// \brief Cup Select
#include "../../k_menu.h"
#include "../../s_sound.h"
#include "../../f_finale.h" // F_WipeStartScreen
#include "../../v_video.h"
#include "../../k_grandprix.h"
#include "../../r_local.h" // SplitScreen_OnChange
menuitem_t PLAY_CupSelect[] =
{
{IT_NOTHING | IT_KEYHANDLER, NULL, NULL, NULL, {.routine = M_CupSelectHandler}, 0, 0},
};
menu_t PLAY_CupSelectDef = {
sizeof(PLAY_CupSelect) / sizeof(menuitem_t),
&PLAY_RaceGamemodesDef,
0,
PLAY_CupSelect,
0, 0,
0, 0,
2, 5,
M_DrawCupSelect,
M_CupSelectTick,
NULL,
NULL,
NULL
};
struct cupgrid_s cupgrid;
void M_CupSelectHandler(INT32 choice)
{
const UINT8 pid = 0;
(void)choice;
if (menucmd[pid].dpad_lr > 0)
{
cupgrid.x++;
if (cupgrid.x >= CUPMENU_COLUMNS)
{
cupgrid.x = 0;
cupgrid.pageno++;
if (cupgrid.pageno >= cupgrid.numpages)
cupgrid.pageno = 0;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_lr < 0)
{
cupgrid.x--;
if (cupgrid.x < 0)
{
cupgrid.x = CUPMENU_COLUMNS-1;
if (cupgrid.pageno == 0)
cupgrid.pageno = cupgrid.numpages-1;
else
cupgrid.pageno--;
}
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
if (menucmd[pid].dpad_ud > 0)
{
cupgrid.y++;
if (cupgrid.y >= CUPMENU_ROWS)
cupgrid.y = 0;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
else if (menucmd[pid].dpad_ud < 0)
{
cupgrid.y--;
if (cupgrid.y < 0)
cupgrid.y = CUPMENU_ROWS-1;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
}
if (M_MenuConfirmPressed(pid) /*|| M_MenuButtonPressed(pid, MBT_START)*/)
{
INT16 count;
cupheader_t *newcup = cupgrid.builtgrid[CUPMENU_CURSORID];
cupheader_t *oldcup = levellist.levelsearch.cup;
M_SetMenuDelay(pid);
levellist.levelsearch.cup = newcup;
count = M_CountLevelsToShowInList(&levellist.levelsearch);
if ((!newcup)
|| (count <= 0)
|| (cupgrid.grandprix == true && newcup->cachedlevels[0] == NEXTMAP_INVALID))
{
S_StartSound(NULL, sfx_s3kb2);
return;
}
if (cupgrid.grandprix == true)
{
INT32 levelNum;
UINT8 ssplayers = cv_splitplayers.value-1;
S_StartSound(NULL, sfx_s3k63);
// Early fadeout to let the sound finish playing
F_WipeStartScreen();
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
F_WipeEndScreen();
F_RunWipe(wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false);
memset(&grandprixinfo, 0, sizeof(struct grandprixinfo));
if (cv_maxconnections.value < ssplayers+1)
CV_SetValue(&cv_maxconnections, ssplayers+1);
if (splitscreen != ssplayers)
{
splitscreen = ssplayers;
SplitScreen_OnChange();
}
// read our dummy cvars
grandprixinfo.gamespeed = min(KARTSPEED_HARD, cv_dummygpdifficulty.value);
grandprixinfo.masterbots = (cv_dummygpdifficulty.value == 3);
grandprixinfo.encore = (boolean)cv_dummygpencore.value;
grandprixinfo.cup = newcup;
grandprixinfo.gp = true;
grandprixinfo.roundnum = 1;
grandprixinfo.initalize = true;
paused = false;
// Don't restart the server if we're already in a game lol
if (gamestate == GS_MENU)
{
SV_StartSinglePlayerServer(levellist.newgametype, levellist.netgame);
}
levelNum = grandprixinfo.cup->cachedlevels[0];
D_MapChange(
levelNum + 1,
GT_RACE,
grandprixinfo.encore,
true,
1,
false,
false
);
M_ClearMenus(true);
}
else if (count == 1)
{
PLAY_TimeAttackDef.transitionID = currentMenu->transitionID+1;
M_LevelSelected(0);
}
else
{
// Keep cursor position if you select the same cup again, reset if it's a different cup
if (oldcup != newcup || levellist.cursor >= count)
{
levellist.cursor = 0;
}
M_LevelSelectScrollDest();
levellist.y = levellist.dest;
M_SetupNextMenu(&PLAY_LevelSelectDef, false);
S_StartSound(NULL, sfx_s3k63);
}
}
else if (M_MenuBackPressed(pid))
{
M_SetMenuDelay(pid);
if (currentMenu->prevMenu)
M_SetupNextMenu(currentMenu->prevMenu, false);
else
M_ClearMenus(true);
}
}
void M_CupSelectTick(void)
{
cupgrid.previewanim++;
}

Some files were not shown because too many files have changed in this diff Show more