Merge branch 'master' into acs

This commit is contained in:
Sally Coolatta 2023-01-05 22:23:51 -05:00
commit 1caf255f5c
140 changed files with 13719 additions and 2955 deletions

View file

@ -130,9 +130,7 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}")
find_package(ZLIB REQUIRED)
find_package(PNG REQUIRED)
find_package(SDL2 REQUIRED)
find_package(SDL2_mixer REQUIRED)
find_package(CURL REQUIRED)
find_package(OPENMPT REQUIRED)
find_package(GME REQUIRED)
endif()

View file

@ -1,3 +1,5 @@
cmake_minimum_required(VERSION 3.3 FATAL_ERROR)
set(CMAKE_BINARY_DIR "${BINARY_DIR}")
set(CMAKE_CURRENT_BINARY_DIR "${BINARY_DIR}")
@ -7,7 +9,23 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Mo
include(GitUtilities)
git_current_branch(SRB2_COMP_BRANCH)
git_summary(SRB2_COMP_REVISION)
git_working_tree_dirty(SRB2_COMP_UNCOMMITTED)
git_summary(revision)
string(REGEX REPLACE "([\"\\])" "\\\\\\1" SRB2_COMP_REVISION "${revision}")
if("${CMAKE_BUILD_TYPE}" STREQUAL "")
set(CMAKE_BUILD_TYPE None)
endif()
# These build types enable optimizations of some kind by default.
set(optimized_build_types "MINSIZEREL;RELEASE;RELWITHDEBINFO")
string(TOUPPER "${CMAKE_BUILD_TYPE}" build_type)
if("${build_type}" IN_LIST optimized_build_types)
set(SRB2_COMP_OPTIMIZED TRUE)
else()
set(SRB2_COMP_OPTIMIZED FALSE)
endif()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/src/config.h")

View file

@ -1,33 +0,0 @@
include(LibFindMacros)
libfind_pkg_check_modules(OPENMPT_PKGCONF OPENMPT)
find_path(OPENMPT_INCLUDE_DIR
NAMES libopenmpt.h
PATHS
${OPENMPT_PKGCONF_INCLUDE_DIRS}
"/usr/include/libopenmpt"
"/usr/local/include/libopenmpt"
)
find_library(OPENMPT_LIBRARY
NAMES openmpt
PATHS
${OPENMPT_PKGCONF_LIBRARY_DIRS}
"/usr/lib"
"/usr/local/lib"
)
set(OPENMPT_PROCESS_INCLUDES OPENMPT_INCLUDE_DIR)
set(OPENMPT_PROCESS_LIBS OPENMPT_LIBRARY)
libfind_process(OPENMPT)
if(OPENMPT_FOUND AND NOT TARGET openmpt)
add_library(openmpt UNKNOWN IMPORTED)
set_target_properties(
openmpt
PROPERTIES
IMPORTED_LOCATION "${OPENMPT_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${OPENMPT_INCLUDE_DIR}"
)
endif()

View file

@ -1,44 +0,0 @@
# Find SDL2
# Once done, this will define
#
# SDL2_MIXER_FOUND - system has SDL2
# SDL2_MIXER_INCLUDE_DIRS - SDL2 include directories
# SDL2_MIXER_LIBRARIES - link libraries
include(LibFindMacros)
libfind_pkg_check_modules(SDL2_MIXER_PKGCONF SDL2_mixer)
# includes
find_path(SDL2_MIXER_INCLUDE_DIR
NAMES SDL_mixer.h
PATHS
${SDL2_MIXER_PKGCONF_INCLUDE_DIRS}
"/usr/include/SDL2"
"/usr/local/include/SDL2"
)
# library
find_library(SDL2_MIXER_LIBRARY
NAMES SDL2_mixer
PATHS
${SDL2_MIXER_PKGCONF_LIBRARY_DIRS}
"/usr/lib"
"/usr/local/lib"
)
# set include dir variables
set(SDL2_MIXER_PROCESS_INCLUDES SDL2_MIXER_INCLUDE_DIR)
set(SDL2_MIXER_PROCESS_LIBS SDL2_MIXER_LIBRARY)
libfind_process(SDL2_MIXER)
if(SDL2_MIXER_FOUND AND NOT TARGET SDL2_mixer::SDL2_mixer)
add_library(SDL2_mixer::SDL2_mixer UNKNOWN IMPORTED)
set_target_properties(
SDL2_mixer::SDL2_mixer
PROPERTIES
IMPORTED_LOCATION "${SDL2_MIXER_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_MIXER_INCLUDE_DIR}"
)
endif()

View file

@ -154,7 +154,7 @@ add_custom_command(
# build time for accurate git information and before anything
# that needs it, obviously.
add_custom_target(_SRB2_reconf ALL
COMMAND ${CMAKE_COMMAND} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}/.. -P ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/Comptime.cmake
COMMAND ${CMAKE_COMMAND} -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}/.. -P ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/Comptime.cmake
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.."
)
add_dependencies(SRB2SDL2 _SRB2_reconf)
@ -214,9 +214,6 @@ if(NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
target_include_directories(SRB2SDL2 PRIVATE "${libgme_SOURCE_DIR}")
endif()
target_link_libraries(SRB2SDL2 PRIVATE openmpt)
target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_OPENMPT)
target_link_libraries(SRB2SDL2 PRIVATE ZLIB::ZLIB PNG::PNG CURL::libcurl)
target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_ZLIB -DHAVE_PNG -DHAVE_CURL -D_LARGEFILE64_SOURCE)
target_sources(SRB2SDL2 PRIVATE apng.c)
@ -226,6 +223,9 @@ target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_DISCORDRPC -DUSE_STUN)
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 acsvm::acsvm)
set(SRB2_HAVE_THREADS ON)
@ -312,15 +312,6 @@ if("${SRB2_CONFIG_HWRENDER}")
endif()
endif()
# TODO: build this with the game
if(${CMAKE_SYSTEM} MATCHES Windows AND ${CMAKE_C_COMPILER_ID} MATCHES "GNU" AND ${SRB2_SYSTEM_BITS} EQUAL 32)
target_link_libraries(SRB2SDL2 PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../libs/drmingw/lib/win32/libexchndl.a"
"${CMAKE_CURRENT_SOURCE_DIR}/../libs/drmingw/lib/win32/libmgwhelp.a"
)
target_include_directories(SRB2SDL2 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../libs/drmingw/include")
endif()
if(${SRB2_CONFIG_USEASM})
#SRB2_ASM_FLAGS can be used to pass flags to either nasm or yasm.
if("${CMAKE_SYSTEM_NAME}" MATCHES "Linux")
@ -403,7 +394,7 @@ target_compile_options(SRB2SDL2 PRIVATE
-Winline
-Wformat-y2k
-Wformat-security
$<$<VERSION_LESS:$<C_COMPILER_VERSION>,2.9.5>:
-Wno-div-by-zero
-Wendif-labels
@ -539,6 +530,7 @@ if(SRB2_CONFIG_PROFILEMODE AND "${CMAKE_C_COMPILER_ID}" STREQUAL "GNU")
target_link_options(SRB2SDL2 PRIVATE -pg)
endif()
add_subdirectory(audio)
add_subdirectory(io)
add_subdirectory(sdl)
add_subdirectory(objects)

37
src/audio/CMakeLists.txt Normal file
View file

@ -0,0 +1,37 @@
target_sources(SRB2SDL2 PRIVATE
chunk_load.cpp
chunk_load.hpp
expand_mono.cpp
expand_mono.hpp
filter.cpp
filter.hpp
gain.cpp
gain.hpp
gme_player.cpp
gme_player.hpp
gme.cpp
gme.hpp
mixer.cpp
mixer.hpp
music_player.cpp
music_player.hpp
ogg_player.cpp
ogg_player.hpp
ogg.cpp
ogg.hpp
resample.cpp
resample.hpp
sample.hpp
sound_chunk.hpp
sound_effect_player.cpp
sound_effect_player.hpp
source.hpp
wav_player.cpp
wav_player.hpp
wav.cpp
wav.hpp
xmp_player.cpp
xmp_player.hpp
xmp.cpp
xmp.hpp
)

206
src/audio/chunk_load.cpp Normal file
View file

@ -0,0 +1,206 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "chunk_load.hpp"
#include <stb_vorbis.h>
#include "../cxxutil.hpp"
#include "../io/streams.hpp"
#include "gme.hpp"
#include "gme_player.hpp"
#include "ogg.hpp"
#include "ogg_player.hpp"
#include "resample.hpp"
#include "sound_chunk.hpp"
#include "sound_effect_player.hpp"
#include "wav.hpp"
#include "wav_player.hpp"
using std::nullopt;
using std::optional;
using std::size_t;
using namespace srb2::audio;
using namespace srb2;
namespace {
// Utility for leveraging Resampler...
class SoundChunkSource : public Source<1> {
public:
explicit SoundChunkSource(std::unique_ptr<SoundChunk>&& chunk)
: chunk_(std::forward<std::unique_ptr<SoundChunk>>(chunk)) {}
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_++) {
buffer[written] = chunk_->samples[pos_];
written++;
}
return written;
}
private:
std::unique_ptr<SoundChunk> chunk_;
size_t pos_ {0};
};
template <class I>
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 {
generated.resize(total + 4096);
read = source.generate(tcb::span {generated.data() + total, 4096});
total += read;
} while (read != 0);
generated.resize(total);
return generated;
}
optional<SoundChunk> try_load_dmx(tcb::span<std::byte> data) {
io::SpanStream stream {data};
if (io::remaining(stream) < 8)
return nullopt;
uint16_t version = io::read_uint16(stream);
if (version != 3)
return nullopt;
uint16_t rate = io::read_uint16(stream);
uint32_t length = io::read_uint32(stream) - 32u;
if (io::remaining(stream) < (length + 32u))
return nullopt;
stream.seek(io::SeekFrom::kCurrent, 16);
std::vector<Sample<1>> samples;
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) {
return SoundChunk {samples};
}
std::unique_ptr<SoundChunkSource> chunk_source =
std::make_unique<SoundChunkSource>(std::make_unique<SoundChunk>(SoundChunk {std::move(samples)}));
Resampler<1> resampler(std::move(chunk_source), rate / static_cast<float>(kSampleRate));
std::vector<Sample<1>> resampled;
size_t total = 0;
size_t read = 0;
resampled.reserve(samples_len * (static_cast<float>(kSampleRate) / rate));
do {
resampled.resize(total + 4096);
read = resampler.generate(tcb::span {resampled.data() + total, 4096});
total += read;
} while (read != 0);
resampled.resize(total);
return SoundChunk {std::move(resampled)};
}
optional<SoundChunk> try_load_wav(tcb::span<std::byte> data) {
io::SpanStream stream {data};
audio::Wav wav;
std::size_t sample_rate;
try {
wav = audio::load_wav(stream);
} 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));
SoundChunk chunk {generate_to_vec(resampler)};
return chunk;
}
optional<SoundChunk> try_load_ogg(tcb::span<std::byte> data) {
std::shared_ptr<audio::OggPlayer<1>> player;
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 (...) {
return nullopt;
}
player->looping(false);
player->playing(true);
player->reset();
std::size_t sample_rate = player->sample_rate();
audio::Resampler<1> resampler(player, sample_rate / 44100.);
std::vector<Sample<1>> resampled {generate_to_vec(resampler)};
SoundChunk chunk {std::move(resampled)};
return chunk;
}
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}) {
io::SpanStream stream {data};
audio::Gme gme = audio::load_gme(stream);
player = std::make_shared<GmePlayer<1>>(std::move(gme));
} else {
io::ZlibInputStream stream {io::SpanStream(data)};
audio::Gme gme = audio::load_gme(stream);
player = std::make_shared<GmePlayer<1>>(std::move(gme));
}
} catch (...) {
return nullopt;
}
std::vector<Sample<1>> samples {generate_to_vec(*player)};
SoundChunk chunk {std::move(samples)};
return chunk;
}
} // namespace
optional<SoundChunk> srb2::audio::try_load_chunk(tcb::span<std::byte> data) {
optional<SoundChunk> ret;
ret = try_load_dmx(data);
if (ret)
return ret;
ret = try_load_wav(data);
if (ret)
return ret;
ret = try_load_ogg(data);
if (ret)
return ret;
ret = try_load_gme(data);
if (ret)
return ret;
return nullopt;
}

27
src/audio/chunk_load.hpp Normal file
View file

@ -0,0 +1,27 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_CHUNK_LOAD_HPP__
#define __SRB2_AUDIO_CHUNK_LOAD_HPP__
#include <cstddef>
#include <optional>
#include <tcb/span.hpp>
#include "sound_chunk.hpp"
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);
} // namespace srb2::audio
#endif // __SRB2_AUDIO_CHUNK_LOAD_HPP__

26
src/audio/expand_mono.cpp Normal file
View file

@ -0,0 +1,26 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "expand_mono.hpp"
#include <algorithm>
using std::size_t;
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++) {
buffer[i].amplitudes[0] = input_buffer[i].amplitudes[0];
buffer[i].amplitudes[1] = input_buffer[i].amplitudes[0];
}
return std::min(input_buffer.size(), buffer.size());
}

27
src/audio/expand_mono.hpp Normal file
View file

@ -0,0 +1,27 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_EXPAND_MONO_HPP__
#define __SRB2_AUDIO_EXPAND_MONO_HPP__
#include <tcb/span.hpp>
#include "filter.hpp"
namespace srb2::audio {
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;
};
} // namespace srb2::audio
#endif // __SRB2_AUDIO_EXPAND_MONO_HPP__

40
src/audio/filter.cpp Normal file
View file

@ -0,0 +1,40 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "filter.hpp"
using std::shared_ptr;
using std::size_t;
using srb2::audio::Filter;
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) {
input_buffer_.clear();
input_buffer_.resize(buffer.size());
input_->generate(input_buffer_);
return filter(input_buffer_, buffer);
}
template <size_t IC, size_t OC>
void Filter<IC, OC>::bind(const shared_ptr<Source<IC>>& input) {
input_ = input;
}
template <size_t IC, size_t OC>
Filter<IC, OC>::~Filter() = default;
template class srb2::audio::Filter<1, 1>;
template class srb2::audio::Filter<1, 2>;
template class srb2::audio::Filter<2, 1>;
template class srb2::audio::Filter<2, 2>;

46
src/audio/filter.hpp Normal file
View file

@ -0,0 +1,46 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_FILTER_HPP__
#define __SRB2_AUDIO_FILTER_HPP__
#include <cstddef>
#include <memory>
#include <vector>
#include <tcb/span.hpp>
#include "source.hpp"
namespace srb2::audio {
template <size_t IC, size_t OC>
class Filter : public Source<OC> {
public:
virtual std::size_t generate(tcb::span<Sample<OC>> buffer) override;
void bind(const std::shared_ptr<Source<IC>>& input);
virtual std::size_t filter(tcb::span<Sample<IC>> input_buffer, tcb::span<Sample<OC>> buffer) = 0;
virtual ~Filter();
private:
std::shared_ptr<Source<IC>> input_;
std::vector<Sample<IC>> input_buffer_;
};
extern template class Filter<1, 1>;
extern template class Filter<1, 2>;
extern template class Filter<2, 1>;
extern template class Filter<2, 2>;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_FILTER_HPP__

43
src/audio/gain.cpp Normal file
View file

@ -0,0 +1,43 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "gain.hpp"
#include <algorithm>
using std::size_t;
using srb2::audio::Filter;
using srb2::audio::Gain;
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 written = std::min(buffer.size(), input_buffer.size());
for (size_t i = 0; i < written; i++) {
buffer[i] = input_buffer[i];
buffer[i] *= gain_;
gain_ += (new_gain_ - gain_) * kGainInterpolationAlpha;
}
return written;
}
template <size_t C>
void Gain<C>::gain(float new_gain) {
new_gain_ = std::clamp(new_gain, 0.0f, 1.0f);
}
template <size_t C>
Gain<C>::~Gain() = default;
template class srb2::audio::Gain<1>;
template class srb2::audio::Gain<2>;

33
src/audio/gain.hpp Normal file
View file

@ -0,0 +1,33 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_GAIN_HPP__
#define __SRB2_AUDIO_GAIN_HPP__
#include <tcb/span.hpp>
#include "filter.hpp"
namespace srb2::audio {
template <size_t 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);
virtual ~Gain();
private:
float new_gain_ {1.f};
float gain_ {1.f};
};
} // namespace srb2::audio
#endif // __SRB2_AUDIO_GAIN_HPP__

141
src/audio/gme.cpp Normal file
View file

@ -0,0 +1,141 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "gme.hpp"
#include <limits>
#include <stdexcept>
#include "../cxxutil.hpp"
using namespace srb2;
using namespace srb2::audio;
Gme::Gme() : 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) {
_init_with_data();
}
Gme::Gme(tcb::span<std::byte> data) : memory_data_(data.begin(), data.end()), instance_(nullptr) {
_init_with_data();
}
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_delete(instance_);
instance_ = nullptr;
}
}
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());
if (err)
throw GmeException(err);
return buffer.size();
}
void Gme::seek(int sample) {
SRB2_ASSERT(instance_ != nullptr);
gme_seek_samples(instance_, sample);
}
std::optional<float> Gme::duration_seconds() const {
SRB2_ASSERT(instance_ != nullptr);
gme_info_t* info = nullptr;
gme_err_t res = gme_track_info(instance_, &info, 0);
if (res)
throw GmeException(res);
auto info_finally = srb2::finally([&info] { gme_free_info(info); });
if (info->length == -1)
return std::nullopt;
// info lengths are in ms
return static_cast<float>(info->length) / 1000.f;
}
std::optional<float> Gme::loop_point_seconds() const {
SRB2_ASSERT(instance_ != nullptr);
gme_info_t* info = nullptr;
gme_err_t res = gme_track_info(instance_, &info, 0);
if (res)
throw GmeException(res);
auto info_finally = srb2::finally([&info] { gme_free_info(info); });
int loop_point_ms = info->intro_length;
if (loop_point_ms == -1)
return std::nullopt;
return loop_point_ms / 44100.f;
}
float Gme::position_seconds() const {
SRB2_ASSERT(instance_ != nullptr);
gme_info_t* info = nullptr;
gme_err_t res = gme_track_info(instance_, &info, 0);
if (res)
throw GmeException(res);
auto info_finally = srb2::finally([&info] { gme_free_info(info); });
int position = gme_tell(instance_);
// adjust position, since GME's counter keeps going past loop
if (info->length > 0)
position %= info->length;
else if (info->intro_length + info->loop_length > 0)
position = position >= (info->intro_length + info->loop_length) ? (position % info->loop_length) : position;
else
position %= 150 * 1000; // 2.5 minutes
return position / 1000.f;
}
void Gme::_init_with_data() {
if (instance_) {
return;
}
if (memory_data_.size() >= std::numeric_limits<long>::max())
throw std::invalid_argument("Buffer is too large for gme");
if (memory_data_.size() == 0)
throw std::invalid_argument("Insufficient data from stream");
gme_err_t result =
gme_open_data(reinterpret_cast<const void*>(memory_data_.data()), memory_data_.size(), &instance_, 44100);
if (result)
throw GmeException(result);
// we no longer need the data, so there's no reason to keep the allocation
memory_data_ = std::vector<std::byte>();
result = gme_start_track(instance_, 0);
if (result)
throw GmeException(result);
}

74
src/audio/gme.hpp Normal file
View file

@ -0,0 +1,74 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_GME_HPP__
#define __SRB2_AUDIO_GME_HPP__
#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <gme/gme.h>
#undef byte // BLARGG!! NO!!
#undef check // STOP IT!!!!
#include "../io/streams.hpp"
namespace srb2::audio {
class GmeException : public std::exception {
std::string msg_;
public:
explicit GmeException(gme_err_t msg) : msg_(msg == nullptr ? "" : msg) {}
virtual const char* what() const noexcept override { return msg_.c_str(); }
};
class Gme {
std::vector<std::byte> memory_data_;
Music_Emu* instance_;
public:
Gme();
Gme(const Gme&) = delete;
Gme(Gme&& rhs) noexcept;
Gme& operator=(const Gme&) = delete;
Gme& operator=(Gme&& rhs) noexcept;
explicit Gme(std::vector<std::byte>&& data);
explicit Gme(tcb::span<std::byte> data);
std::size_t get_samples(tcb::span<short> buffer);
void seek(int sample);
std::optional<float> duration_seconds() const;
std::optional<float> loop_point_seconds() const;
float position_seconds() const;
~Gme();
private:
void _init_with_data();
};
template <typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Gme load_gme(I& stream) {
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Gme {std::move(data)};
}
} // namespace srb2::audio
#endif // __SRB2_AUDIO_GME_HPP__

73
src/audio/gme_player.cpp Normal file
View file

@ -0,0 +1,73 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "gme_player.hpp"
using namespace srb2;
using namespace srb2::audio;
template <size_t C>
GmePlayer<C>::GmePlayer(Gme&& gme) : gme_(std::forward<Gme>(gme)), buf_() {
}
template <size_t C>
GmePlayer<C>::GmePlayer(GmePlayer<C>&& rhs) noexcept = default;
template <size_t C>
GmePlayer<C>& GmePlayer<C>::operator=(GmePlayer<C>&& rhs) noexcept = default;
template <size_t C>
GmePlayer<C>::~GmePlayer() = default;
template <size_t C>
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) {
buffer[i].amplitudes[0] = (buf_[i * 2] / 32768.f + buf_[i * 2 + 1] / 32768.f) / 2.f;
} else if constexpr (C == 2) {
buffer[i].amplitudes[0] = buf_[i * 2] / 32768.f;
buffer[i].amplitudes[1] = buf_[i * 2 + 1] / 32768.f;
}
}
return new_samples;
}
template <size_t C>
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() {
gme_.seek(0);
}
template <size_t C>
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 {
return gme_.loop_point_seconds();
}
template <size_t C>
float GmePlayer<C>::position_seconds() const {
return gme_.position_seconds();
}
template class srb2::audio::GmePlayer<1>;
template class srb2::audio::GmePlayer<2>;

51
src/audio/gme_player.hpp Normal file
View file

@ -0,0 +1,51 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_GME_PLAYER_HPP__
#define __SRB2_AUDIO_GME_PLAYER_HPP__
#include <optional>
#include "gme.hpp"
#include "source.hpp"
namespace srb2::audio {
template <size_t C>
class GmePlayer : public Source<C> {
Gme gme_;
std::vector<short> buf_;
public:
GmePlayer(Gme&& gme);
GmePlayer(const GmePlayer<C>&) = delete;
GmePlayer(GmePlayer<C>&& gme) noexcept;
~GmePlayer();
GmePlayer& operator=(const GmePlayer<C>&) = delete;
GmePlayer& operator=(GmePlayer<C>&& rhs) noexcept;
virtual std::size_t generate(tcb::span<Sample<C>> buffer) override;
void seek(float position_seconds);
std::optional<float> duration_seconds() const;
std::optional<float> loop_point_seconds() const;
float position_seconds() const;
void reset();
};
extern template class GmePlayer<1>;
extern template class GmePlayer<2>;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_GME_PLAYER_HPP__

62
src/audio/mixer.cpp Normal file
View file

@ -0,0 +1,62 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "mixer.hpp"
#include <algorithm>
using std::shared_ptr;
using std::size_t;
using srb2::audio::Mixer;
using srb2::audio::Sample;
using srb2::audio::Source;
namespace {
template <size_t C>
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++) {
dst[i] += src[i];
}
}
} // namespace
template <size_t C>
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_) {
size_t read = source->generate(buffer_);
mix_sample_buffers<C>(buffer.data(), buffer.size(), buffer_.data(), read);
}
// because we initialized the out-buffer, we always generate size samples
return buffer.size();
}
template <size_t C>
void Mixer<C>::add_source(const shared_ptr<Source<C>>& source) {
sources_.push_back(source);
}
template <size_t C>
Mixer<C>::~Mixer() = default;
template class srb2::audio::Mixer<1>;
template class srb2::audio::Mixer<2>;

41
src/audio/mixer.hpp Normal file
View file

@ -0,0 +1,41 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_MIXER_HPP__
#define __SRB2_AUDIO_MIXER_HPP__
#include <memory>
#include <vector>
#include <tcb/span.hpp>
#include "source.hpp"
namespace srb2::audio {
template <size_t C>
class Mixer : public Source<C> {
public:
virtual std::size_t generate(tcb::span<Sample<C>> buffer) override final;
virtual ~Mixer();
void add_source(const std::shared_ptr<Source<C>>& source);
private:
std::vector<std::shared_ptr<Source<C>>> sources_;
std::vector<Sample<C>> buffer_;
};
extern template class Mixer<1>;
extern template class Mixer<2>;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_MIXER_HPP__

422
src/audio/music_player.cpp Normal file
View file

@ -0,0 +1,422 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "music_player.hpp"
#include <algorithm>
#include <cmath>
#include <exception>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include <gme/gme.h>
#include <stb_vorbis.h>
#undef byte // BLARGG!! NO!!
#undef check // STOP IT!!!!
#include "../cxxutil.hpp"
#include "../io/streams.hpp"
#include "gme_player.hpp"
#include "ogg_player.hpp"
#include "resample.hpp"
#include "xmp_player.hpp"
using std::array;
using std::byte;
using std::make_unique;
using std::size_t;
using std::vector;
using srb2::audio::MusicPlayer;
using srb2::audio::Resampler;
using srb2::audio::Sample;
using srb2::audio::Source;
using namespace srb2;
class MusicPlayer::Impl {
public:
Impl() = default;
Impl(tcb::span<std::byte> data) : Impl() { _load(data); }
size_t generate(tcb::span<Sample<2>> buffer) {
if (!resampler_)
return 0;
if (!playing_)
return 0;
size_t total_written = 0;
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++) {
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_;
buffer[total_written + i] *= fade_gain;
}
gain_samples_ = std::min(gain_samples_ + generated, gain_samples_target_);
if (gain_samples_ >= gain_samples_target_) {
fading_ = false;
gain_samples_ = gain_samples_target_;
gain_ = gain_target_;
}
total_written += generated;
if (generated == 0) {
playing_ = false;
break;
}
}
return total_written;
}
void _load(tcb::span<std::byte> data) {
ogg_inst_ = nullptr;
gme_inst_ = nullptr;
xmp_inst_ = nullptr;
resampler_ = std::nullopt;
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) {
// 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}) {
io::ZlibInputStream stream {io::SpanStream(data)};
audio::Gme gme = audio::load_gme(stream);
gme_inst_ = std::make_shared<GmePlayer<2>>(std::move(gme));
} 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) {
// it's probably not gme
gme_inst_ = nullptr;
resampler_ = std::nullopt;
}
}
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) {
// it's probably not xmp
xmp_inst_ = nullptr;
resampler_ = std::nullopt;
}
}
playing_ = false;
internal_gain(1.f);
}
void play(bool looping) {
if (ogg_inst_) {
ogg_inst_->looping(looping);
ogg_inst_->playing(true);
playing_ = true;
ogg_inst_->reset();
} else if (gme_inst_) {
playing_ = true;
gme_inst_->reset();
} else if (xmp_inst_) {
xmp_inst_->looping(looping);
playing_ = true;
xmp_inst_->reset();
}
}
void unpause() {
if (ogg_inst_) {
ogg_inst_->playing(true);
playing_ = true;
} else if (gme_inst_) {
playing_ = true;
} else if (xmp_inst_) {
playing_ = true;
}
}
void pause() {
if (ogg_inst_) {
ogg_inst_->playing(false);
playing_ = false;
} else if (gme_inst_) {
playing_ = false;
} else if (xmp_inst_) {
playing_ = false;
}
}
void stop() {
if (ogg_inst_) {
ogg_inst_->reset();
ogg_inst_->playing(false);
playing_ = false;
} else if (gme_inst_) {
gme_inst_->reset();
playing_ = false;
} else if (xmp_inst_) {
xmp_inst_->reset();
playing_ = false;
}
}
void seek(float position_seconds) {
if (ogg_inst_) {
ogg_inst_->seek(position_seconds);
return;
}
if (gme_inst_) {
gme_inst_->seek(position_seconds);
return;
}
if (xmp_inst_) {
xmp_inst_->seek(position_seconds);
return;
}
}
bool playing() const {
if (ogg_inst_)
return ogg_inst_->playing();
else if (gme_inst_)
return playing_;
else if (xmp_inst_)
return playing_;
return false;
}
std::optional<audio::MusicType> music_type() const {
if (ogg_inst_)
return audio::MusicType::kOgg;
else if (gme_inst_)
return audio::MusicType::kGme;
else if (xmp_inst_)
return audio::MusicType::kMod;
return std::nullopt;
}
std::optional<float> duration_seconds() const {
if (ogg_inst_)
return ogg_inst_->duration_seconds();
if (gme_inst_)
return gme_inst_->duration_seconds();
if (xmp_inst_)
return xmp_inst_->duration_seconds();
return std::nullopt;
}
std::optional<float> loop_point_seconds() const {
if (ogg_inst_)
return ogg_inst_->loop_point_seconds();
if (gme_inst_)
return gme_inst_->loop_point_seconds();
return std::nullopt;
}
std::optional<float> position_seconds() const {
if (ogg_inst_)
return ogg_inst_->position_seconds();
if (gme_inst_)
return gme_inst_->position_seconds();
return std::nullopt;
}
void fade_to(float gain, float seconds) { fade_from_to(gain_target_, gain, 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_ = 0;
}
bool fading() const { return fading_; }
void stop_fade() { internal_gain(gain_target_); }
void loop_point_seconds(float loop_point) {
if (ogg_inst_)
ogg_inst_->loop_point_seconds(loop_point);
}
void internal_gain(float gain) {
fading_ = false;
gain_ = gain;
gain_target_ = gain;
gain_samples_target_ = 1;
gain_samples_ = 0;
}
private:
std::shared_ptr<OggPlayer<2>> ogg_inst_;
std::shared_ptr<GmePlayer<2>> gme_inst_;
std::shared_ptr<XmpPlayer<2>> xmp_inst_;
std::optional<Resampler<2>> resampler_;
bool playing_ {false};
bool looping_ {false};
// fade control
float gain_target_ {1.f};
float gain_ {1.f};
bool fading_ {false};
uint64_t gain_samples_ {0};
uint64_t gain_samples_target_ {1};
};
// The special member functions MUST be declared in this unit, where Impl is complete.
MusicPlayer::MusicPlayer() : impl_(make_unique<MusicPlayer::Impl>()) {
}
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) {
SRB2_ASSERT(impl_ != nullptr);
return impl_->play(looping);
}
void MusicPlayer::unpause() {
SRB2_ASSERT(impl_ != nullptr);
return impl_->unpause();
}
void MusicPlayer::pause() {
SRB2_ASSERT(impl_ != nullptr);
return impl_->pause();
}
void MusicPlayer::stop() {
SRB2_ASSERT(impl_ != nullptr);
return impl_->stop();
}
void MusicPlayer::seek(float position_seconds) {
SRB2_ASSERT(impl_ != nullptr);
return impl_->seek(position_seconds);
}
bool MusicPlayer::playing() const {
SRB2_ASSERT(impl_ != nullptr);
return impl_->playing();
}
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 {
SRB2_ASSERT(impl_ != nullptr);
return impl_->music_type();
}
std::optional<float> MusicPlayer::duration_seconds() const {
SRB2_ASSERT(impl_ != nullptr);
return impl_->duration_seconds();
}
std::optional<float> MusicPlayer::loop_point_seconds() const {
SRB2_ASSERT(impl_ != nullptr);
return impl_->loop_point_seconds();
}
std::optional<float> MusicPlayer::position_seconds() const {
SRB2_ASSERT(impl_ != nullptr);
return impl_->position_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) {
SRB2_ASSERT(impl_ != nullptr);
impl_->fade_from_to(from, to, seconds);
}
void MusicPlayer::internal_gain(float gain) {
SRB2_ASSERT(impl_ != nullptr);
impl_->internal_gain(gain);
}
bool MusicPlayer::fading() const {
SRB2_ASSERT(impl_ != nullptr);
return impl_->fading();
}
void MusicPlayer::stop_fade() {
SRB2_ASSERT(impl_ != nullptr);
impl_->stop_fade();
}
void MusicPlayer::loop_point_seconds(float loop_point) {
SRB2_ASSERT(impl_ != nullptr);
impl_->loop_point_seconds(loop_point);
}

View file

@ -0,0 +1,69 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_MUSIC_PLAYER_HPP__
#define __SRB2_AUDIO_MUSIC_PLAYER_HPP__
#include <memory>
#include <optional>
#include <tcb/span.hpp>
#include "source.hpp"
struct stb_vorbis;
namespace srb2::audio {
enum class MusicType {
kOgg,
kGme,
kMod
};
class MusicPlayer : public Source<2> {
public:
MusicPlayer();
MusicPlayer(tcb::span<std::byte> data);
MusicPlayer(const MusicPlayer& rhs) = delete;
MusicPlayer(MusicPlayer&& rhs) noexcept;
MusicPlayer& operator=(const MusicPlayer& rhs) = delete;
MusicPlayer& operator=(MusicPlayer&& rhs) noexcept;
virtual std::size_t generate(tcb::span<Sample<2>> buffer) override final;
void play(bool looping);
void unpause();
void pause();
void stop();
void seek(float position_seconds);
void fade_to(float gain, float seconds);
void fade_from_to(float from, float to, float seconds);
void internal_gain(float gain);
void stop_fade();
void loop_point_seconds(float loop_point);
bool playing() const;
std::optional<MusicType> music_type() const;
std::optional<float> duration_seconds() const;
std::optional<float> loop_point_seconds() const;
std::optional<float> position_seconds() const;
bool fading() const;
virtual ~MusicPlayer() final;
private:
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace srb2::audio
#endif // __SRB2_AUDIO_MUSIC_PLAYER_HPP__

194
src/audio/ogg.cpp Normal file
View file

@ -0,0 +1,194 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "ogg.hpp"
#include <limits>
#include "../cxxutil.hpp"
using namespace srb2;
using namespace srb2::audio;
StbVorbisException::StbVorbisException(int code) noexcept : code_(code) {
}
const char* StbVorbisException::what() const noexcept {
switch (code_) {
case VORBIS__no_error:
return "No error";
case VORBIS_need_more_data:
return "Need more data";
case VORBIS_invalid_api_mixing:
return "Invalid API mixing";
case VORBIS_outofmem:
return "Out of memory";
case VORBIS_feature_not_supported:
return "Feature not supported";
case VORBIS_too_many_channels:
return "Too many channels";
case VORBIS_file_open_failure:
return "File open failure";
case VORBIS_seek_without_length:
return "Seek without length";
case VORBIS_unexpected_eof:
return "Unexpected EOF";
case VORBIS_seek_invalid:
return "Seek invalid";
case VORBIS_invalid_setup:
return "Invalid setup";
case VORBIS_invalid_stream:
return "Invalid stream";
case VORBIS_missing_capture_pattern:
return "Missing capture pattern";
case VORBIS_invalid_stream_structure_version:
return "Invalid stream structure version";
case VORBIS_continued_packet_flag_invalid:
return "Continued packet flag invalid";
case VORBIS_incorrect_stream_serial_number:
return "Incorrect stream serial number";
case VORBIS_invalid_first_page:
return "Invalid first page";
case VORBIS_bad_packet_type:
return "Bad packet type";
case VORBIS_cant_find_last_page:
return "Can't find last page";
case VORBIS_seek_failed:
return "Seek failed";
case VORBIS_ogg_skeleton_not_supported:
return "OGG skeleton not supported";
default:
return "Unrecognized error code";
}
}
Ogg::Ogg() noexcept : memory_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) {
_init_with_data();
}
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 {
std::swap(memory_data_, rhs.memory_data_);
std::swap(instance_, rhs.instance_);
return *this;
}
Ogg::~Ogg() {
if (instance_) {
stb_vorbis_close(instance_);
instance_ = nullptr;
}
}
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);
return read;
}
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);
stb_vorbis_info info = stb_vorbis_get_info(instance_);
if (info.channels == 1) {
for (auto& sample : buffer.subspan(0, read)) {
sample.amplitudes[1] = sample.amplitudes[0];
}
}
return read;
}
OggComment Ogg::comment() const {
SRB2_ASSERT(instance_ != nullptr);
stb_vorbis_comment c_comment = stb_vorbis_get_comment(instance_);
return OggComment {
std::string(c_comment.vendor),
std::vector<std::string>(c_comment.comment_list, c_comment.comment_list + c_comment.comment_list_length)};
}
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) {
SRB2_ASSERT(instance_ != nullptr);
stb_vorbis_seek(instance_, sample);
}
std::size_t Ogg::position() const {
SRB2_ASSERT(instance_ != nullptr);
return stb_vorbis_get_sample_offset(instance_);
}
float Ogg::position_seconds() const {
return position() / static_cast<float>(sample_rate());
}
std::size_t Ogg::duration_samples() const {
SRB2_ASSERT(instance_ != nullptr);
return stb_vorbis_stream_length_in_samples(instance_);
}
float Ogg::duration_seconds() const {
SRB2_ASSERT(instance_ != nullptr);
return stb_vorbis_stream_length_in_seconds(instance_);
}
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_) {
return;
}
if (memory_data_.size() >= std::numeric_limits<int>::max())
throw std::logic_error("Buffer is too large for stb_vorbis");
if (memory_data_.size() == 0)
throw std::logic_error("Insufficient data from stream");
int vorbis_result;
instance_ = stb_vorbis_open_memory(
reinterpret_cast<const unsigned char*>(memory_data_.data()), memory_data_.size(), &vorbis_result, NULL);
if (vorbis_result != VORBIS__no_error)
throw StbVorbisException(vorbis_result);
}

81
src/audio/ogg.hpp Normal file
View file

@ -0,0 +1,81 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_OGG_HPP__
#define __SRB2_AUDIO_OGG_HPP__
#include <exception>
#include <stdexcept>
#include <vector>
#include <stb_vorbis.h>
#include <tcb/span.hpp>
#include "../io/streams.hpp"
#include "source.hpp"
namespace srb2::audio {
class StbVorbisException final : public std::exception {
int code_;
public:
explicit StbVorbisException(int code) noexcept;
virtual const char* what() const noexcept;
};
struct OggComment {
std::string vendor;
std::vector<std::string> comments;
};
class Ogg final {
std::vector<std::byte> memory_data_;
stb_vorbis* instance_;
public:
Ogg() noexcept;
explicit Ogg(std::vector<std::byte> data);
explicit Ogg(tcb::span<std::byte> data);
Ogg(const Ogg&) = delete;
Ogg(Ogg&& rhs) noexcept;
Ogg& operator=(const Ogg&) = delete;
Ogg& operator=(Ogg&& rhs) noexcept;
~Ogg();
std::size_t get_samples(tcb::span<Sample<1>> buffer);
std::size_t get_samples(tcb::span<Sample<2>> buffer);
void seek(std::size_t sample);
std::size_t position() const;
float position_seconds() const;
OggComment comment() const;
std::size_t sample_rate() const;
std::size_t channels() const;
std::size_t duration_samples() const;
float duration_seconds() const;
private:
void _init_with_data();
};
template <typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Ogg load_ogg(I& stream) {
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Ogg {std::move(data)};
}
} // namespace srb2::audio
#endif // __SRB2_AUDIO_OGG_HPP__

141
src/audio/ogg_player.cpp Normal file
View file

@ -0,0 +1,141 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "ogg_player.hpp"
#include <cmath>
#include <limits>
#include <optional>
#include <stdexcept>
#include <utility>
using namespace srb2;
using namespace srb2::audio;
namespace {
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) {
std::string_view comment_view(comment);
comment_view.remove_prefix(10);
std::string copied {comment_view};
try {
int loop_point = std::stoi(copied);
return loop_point;
} catch (...) {
}
}
if (comment.find("LOOPMS=") == 0) {
std::string_view comment_view(comment);
comment_view.remove_prefix(7);
std::string copied {comment_view};
try {
int loop_ms = std::stoi(copied);
int loop_point = std::round(static_cast<double>(loop_ms) / (rate / 1000.));
return loop_point;
} catch (...) {
}
}
}
return std::nullopt;
}
} // namespace
template <size_t C>
OggPlayer<C>::OggPlayer(Ogg&& ogg) noexcept
: playing_(false), looping_(false), loop_point_(std::nullopt), ogg_(std::forward<Ogg>(ogg)) {
loop_point_ = find_loop_point(ogg_);
}
template <size_t C>
OggPlayer<C>::OggPlayer(OggPlayer&& rhs) noexcept = default;
template <size_t C>
OggPlayer<C>& OggPlayer<C>::operator=(OggPlayer&& rhs) noexcept = default;
template <size_t C>
OggPlayer<C>::~OggPlayer() = default;
template <size_t C>
std::size_t OggPlayer<C>::generate(tcb::span<Sample<C>> buffer) {
if (!playing_)
return 0;
std::size_t total = 0;
do {
std::size_t read = ogg_.get_samples(buffer.subspan(total));
total += read;
if (read == 0 && !looping_) {
playing_ = false;
break;
}
if (read == 0 && loop_point_) {
ogg_.seek(*loop_point_);
}
if (read == 0 && !loop_point_) {
ogg_.seek(0);
}
} while (total < buffer.size());
return total;
}
template <size_t C>
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) {
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() {
ogg_.seek(0);
}
template <size_t C>
std::size_t OggPlayer<C>::sample_rate() const {
return ogg_.sample_rate();
}
template <size_t C>
float OggPlayer<C>::duration_seconds() const {
return ogg_.duration_seconds();
}
template <size_t C>
std::optional<float> OggPlayer<C>::loop_point_seconds() const {
if (!loop_point_)
return std::nullopt;
return *loop_point_ / static_cast<float>(sample_rate());
}
template <size_t C>
float OggPlayer<C>::position_seconds() const {
return ogg_.position_seconds();
}
template class srb2::audio::OggPlayer<1>;
template class srb2::audio::OggPlayer<2>;

72
src/audio/ogg_player.hpp Normal file
View file

@ -0,0 +1,72 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_OGG_SOURCE_HPP__
#define __SRB2_AUDIO_OGG_SOURCE_HPP__
#include <cstddef>
#include <optional>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>
#include <stb_vorbis.h>
#include <tcb/span.hpp>
#include "../io/streams.hpp"
#include "ogg.hpp"
#include "source.hpp"
namespace srb2::audio {
template <size_t C>
class OggPlayer final : public Source<C> {
bool playing_;
bool looping_;
std::optional<std::size_t> loop_point_;
Ogg ogg_;
public:
OggPlayer(Ogg&& ogg) noexcept;
OggPlayer(const OggPlayer&) = delete;
OggPlayer(OggPlayer&& rhs) noexcept;
OggPlayer& operator=(const OggPlayer&) = delete;
OggPlayer& operator=(OggPlayer&& rhs) noexcept;
virtual std::size_t generate(tcb::span<Sample<C>> buffer) override final;
bool looping() const { return looping_; }
void looping(bool looping) { looping_ = looping; }
bool playing() const { return playing_; }
void playing(bool playing) { playing_ = playing; }
void seek(float position_seconds);
void loop_point_seconds(float loop_point);
void reset();
std::size_t sample_rate() const;
float duration_seconds() const;
std::optional<float> loop_point_seconds() const;
float position_seconds() const;
~OggPlayer();
};
extern template class OggPlayer<1>;
extern template class OggPlayer<2>;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_OGG_SOURCE_HPP__

81
src/audio/resample.cpp Normal file
View file

@ -0,0 +1,81 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "resample.hpp"
#include <algorithm>
#include <cmath>
#include <memory>
#include <utility>
#include <vector>
using std::shared_ptr;
using std::size_t;
using std::vector;
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) {
}
template <size_t C>
Resampler<C>::Resampler(Resampler<C>&& r) = default;
template <size_t C>
Resampler<C>::~Resampler() = default;
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) {
if (!source_)
return 0;
if (ratio_ == 1.f) {
// fast path - generate directly from source
size_t source_read = source_->generate(buffer);
return source_read;
}
size_t written = 0;
while (written < buffer.size()) {
// do we need a refill?
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) {
break;
}
}
if (pos_ < 0) {
buffer[written] = (buf_[0] - last_) * pos_frac_ + last_;
advance(ratio_);
written++;
continue;
}
buffer[written] = (buf_[pos_ + 1] - buf_[pos_]) * pos_frac_ + buf_[pos_];
advance(ratio_);
written++;
}
return written;
}
template class srb2::audio::Resampler<1>;
template class srb2::audio::Resampler<2>;

63
src/audio/resample.hpp Normal file
View file

@ -0,0 +1,63 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_RESAMPLE_HPP__
#define __SRB2_AUDIO_RESAMPLE_HPP__
#include <cmath>
#include <memory>
#include <optional>
#include <variant>
#include <vector>
#include <tcb/span.hpp>
#include "sound_chunk.hpp"
#include "source.hpp"
namespace srb2::audio {
template <size_t C>
class Resampler : public Source<C> {
public:
Resampler(std::shared_ptr<Source<C>>&& source_, float ratio);
Resampler(const Resampler<C>& r) = delete;
Resampler(Resampler<C>&& r);
virtual ~Resampler();
virtual std::size_t generate(tcb::span<Sample<C>> buffer);
Resampler& operator=(const Resampler<C>& r) = delete;
Resampler& operator=(Resampler<C>&& r);
private:
std::shared_ptr<Source<C>> source_;
float ratio_ {1.f};
std::vector<Sample<C>> buf_;
Sample<C> last_;
int pos_ {0};
float pos_frac_ {0.f};
void advance(float samples) {
pos_frac_ += samples;
float integer;
std::modf(pos_frac_, &integer);
pos_ += integer;
pos_frac_ -= integer;
}
void refill();
};
extern template class Resampler<1>;
extern template class Resampler<2>;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_RESAMPLE_HPP__

78
src/audio/sample.hpp Normal file
View file

@ -0,0 +1,78 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_SAMPLE_HPP__
#define __SRB2_AUDIO_SAMPLE_HPP__
#include <cstddef>
namespace srb2::audio {
template <size_t C>
struct Sample {
std::array<float, C> amplitudes;
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++) {
amplitudes[i] *= rhs;
}
return *this;
}
};
template <size_t C>
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++) {
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 {
Sample<C> out;
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 {
Sample<C> out;
for (std::size_t i = 0; i < C; i++) {
out.amplitudes[i] = lhs.amplitudes[i] * rhs;
}
return out;
}
template <class T>
static constexpr float sample_to_float(T sample) noexcept;
template <>
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 {
return sample / 32768.f;
}
} // namespace srb2::audio
#endif // __SRB2_AUDIO_SAMPLE_HPP__

25
src/audio/sound_chunk.hpp Normal file
View file

@ -0,0 +1,25 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_SOUND_CHUNK_HPP__
#define __SRB2_AUDIO_SOUND_CHUNK_HPP__
#include <vector>
#include "source.hpp"
namespace srb2::audio {
struct SoundChunk {
std::vector<Sample<1>> samples;
};
} // namespace srb2::audio
#endif // __SRB2_AUDIO_SOUND_CHUNK_HPP__

View file

@ -0,0 +1,72 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "sound_effect_player.hpp"
#include <algorithm>
#include <cmath>
#include <memory>
using std::shared_ptr;
using std::size_t;
using srb2::audio::Sample;
using srb2::audio::SoundEffectPlayer;
using srb2::audio::Source;
size_t SoundEffectPlayer::generate(tcb::span<Sample<2>> buffer) {
if (!chunk_)
return 0;
if (position_ >= chunk_->samples.size()) {
return 0;
}
size_t written = 0;
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);
float left_scale = std::cos(sep_pan);
float right_scale = std::sin(sep_pan);
buffer[written] = {mono_sample * volume_ * left_scale, mono_sample * volume_ * right_scale};
written += 1;
}
return written;
}
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) {
volume_ = volume;
sep_ = sep;
}
void SoundEffectPlayer::reset() {
position_ = 0;
chunk_ = nullptr;
}
bool SoundEffectPlayer::finished() const {
if (!chunk_)
return true;
if (position_ >= chunk_->samples.size())
return true;
return false;
}
bool SoundEffectPlayer::is_playing_chunk(const SoundChunk* chunk) const {
return chunk_ == chunk;
}
SoundEffectPlayer::~SoundEffectPlayer() = default;

View file

@ -0,0 +1,46 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_SOUND_EFFECT_PLAYER_HPP__
#define __SRB2_AUDIO_SOUND_EFFECT_PLAYER_HPP__
#include <cstddef>
#include <tcb/span.hpp>
#include "sound_chunk.hpp"
#include "source.hpp"
namespace srb2::audio {
class SoundEffectPlayer : public Source<2> {
public:
virtual std::size_t generate(tcb::span<Sample<2>> buffer) override final;
virtual ~SoundEffectPlayer() final;
void start(const SoundChunk* chunk, float volume, float sep);
void update(float volume, float sep);
void reset();
bool finished() const;
bool is_playing_chunk(const SoundChunk* chunk) const;
private:
float volume_;
float sep_;
std::size_t position_;
const SoundChunk* chunk_;
};
} // namespace srb2::audio
#endif // __SRB2_AUDIO_SOUND_EFFECT_PLAYER_HPP__

36
src/audio/source.hpp Normal file
View file

@ -0,0 +1,36 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_SOURCE_HPP__
#define __SRB2_AUDIO_SOURCE_HPP__
#include <array>
#include <tcb/span.hpp>
#include "sample.hpp"
namespace srb2::audio {
template <size_t C>
class Source {
public:
virtual std::size_t generate(tcb::span<Sample<C>> buffer) = 0;
virtual ~Source() = default;
};
// This audio DSP is Stereo, FP32 system-endian, 44100 Hz internally.
// Conversions to other formats should be handled elsewhere.
constexpr const std::size_t kSampleRate = 44100;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_SOURCE_HPP__

264
src/audio/wav.cpp Normal file
View file

@ -0,0 +1,264 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "wav.hpp"
#include <algorithm>
#include <optional>
#include <stdexcept>
using namespace srb2;
using srb2::audio::Wav;
namespace {
constexpr const uint32_t kMagicRIFF = 0x46464952;
constexpr const uint32_t kMagicWAVE = 0x45564157;
constexpr const uint32_t kMagicFmt = 0x20746d66;
constexpr const uint32_t kMagicData = 0x61746164;
constexpr const uint16_t kFormatPcm = 1;
constexpr const std::size_t kRiffHeaderLength = 8;
struct RiffHeader {
uint32_t magic;
std::size_t filesize;
};
struct TagHeader {
uint32_t type;
std::size_t length;
};
struct FmtTag {
uint16_t format;
uint16_t channels;
uint32_t rate;
uint32_t bytes_per_second;
uint32_t bytes_per_sample;
uint16_t bit_width;
};
struct DataTag {};
RiffHeader parse_riff_header(io::SpanStream& stream) {
if (io::remaining(stream) < kRiffHeaderLength)
throw std::runtime_error("insufficient bytes remaining in stream");
RiffHeader ret;
ret.magic = io::read_uint32(stream);
ret.filesize = io::read_uint32(stream);
return ret;
}
TagHeader parse_tag_header(io::SpanStream& stream) {
if (io::remaining(stream) < 8)
throw std::runtime_error("insufficient bytes remaining in stream");
TagHeader header;
header.type = io::read_uint32(stream);
header.length = io::read_uint32(stream);
return header;
}
FmtTag parse_fmt_tag(io::SpanStream& stream) {
if (io::remaining(stream) < 16)
throw std::runtime_error("insufficient bytes in stream");
FmtTag tag;
tag.format = io::read_uint16(stream);
tag.channels = io::read_uint16(stream);
tag.rate = io::read_uint32(stream);
tag.bytes_per_second = io::read_uint32(stream);
tag.bytes_per_sample = io::read_uint16(stream);
tag.bit_width = io::read_uint16(stream);
return tag;
}
template <typename Visitor>
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) {
case kMagicFmt:
{
FmtTag fmt_tag {parse_fmt_tag(stream)};
visitor(fmt_tag);
break;
}
case kMagicData:
{
DataTag data_tag;
visitor(data_tag);
break;
}
default:
// Unrecognized tags are ignored.
break;
}
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> samples;
samples.reserve(count);
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> samples;
samples.reserve(count);
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) {
io::SpanStream stream {data};
auto [magic, filesize] = parse_riff_header(stream);
if (magic != kMagicRIFF) {
throw std::runtime_error("invalid RIFF magic");
}
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) {
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) {
TagHeader tag_header {parse_tag_header(stream)};
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) {
throw std::runtime_error("WAVE has multiple 'fmt' tags");
}
if (fmt.format != kFormatPcm) {
throw std::runtime_error("Unsupported WAVE format (only PCM is supported)");
}
read_fmt = 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) {
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) {
case 8:
interleaved_samples = std::move(read_uint8_samples_from_stream(stream, sample_count));
break;
case 16:
interleaved_samples = std::move(read_int16_samples_from_stream(stream, sample_count));
break;
default:
throw std::runtime_error("unsupported sample amplitude bit width");
}
}};
visit_tag(tag_visitor, stream, tag_header);
}
if (!read_fmt) {
throw std::runtime_error("WAVE did not have a fmt tag");
}
interleaved_samples_ = std::move(interleaved_samples);
channels_ = read_fmt->channels;
sample_rate_ = read_fmt->rate;
}
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 {
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) {
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++) {
buffer[i].amplitudes[0] = 0.f;
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);
}
return samples_to_read;
}
} // namespace
std::size_t Wav::get_samples(std::size_t offset, tcb::span<audio::Sample<1>> buffer) const noexcept {
auto samples_visitor = OverloadVisitor {
[&](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);
}};
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(); }};
return std::visit(samples_visitor, interleaved_samples_);
}

51
src/audio/wav.hpp Normal file
View file

@ -0,0 +1,51 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_WAV_HPP__
#define __SRB2_AUDIO_WAV_HPP__
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <variant>
#include <vector>
#include <tcb/span.hpp>
#include "../io/streams.hpp"
#include "sample.hpp"
namespace srb2::audio {
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;
public:
Wav();
explicit Wav(tcb::span<std::byte> data);
std::size_t get_samples(std::size_t offset, tcb::span<Sample<1>> buffer) const noexcept;
std::size_t interleaved_length() const noexcept;
std::size_t length() const noexcept { return interleaved_length() / channels(); };
std::size_t channels() const noexcept { return channels_; };
std::size_t sample_rate() const noexcept { return sample_rate_; };
};
template <typename I, typename std::enable_if_t<srb2::io::IsInputStreamV<I>, int> = 0>
inline Wav load_wav(I& stream) {
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Wav {data};
}
} // namespace srb2::audio
#endif // __SRB2_AUDIO_WAV_HPP__

45
src/audio/wav_player.cpp Normal file
View file

@ -0,0 +1,45 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "wav_player.hpp"
using namespace srb2;
using srb2::audio::WavPlayer;
WavPlayer::WavPlayer() : WavPlayer(audio::Wav {}) {
}
WavPlayer::WavPlayer(const WavPlayer& rhs) = default;
WavPlayer::WavPlayer(WavPlayer&& rhs) noexcept = default;
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) {
}
std::size_t WavPlayer::generate(tcb::span<audio::Sample<1>> buffer) {
std::size_t samples_read = 0;
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_) {
position_ = 0;
}
if (read_this_time == 0 && !looping_) {
break;
}
}
return samples_read;
}

49
src/audio/wav_player.hpp Normal file
View file

@ -0,0 +1,49 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_WAV_PLAYER_HPP__
#define __SRB2_AUDIO_WAV_PLAYER_HPP__
#include <cstddef>
#include <tcb/span.hpp>
#include "source.hpp"
#include "wav.hpp"
namespace srb2::audio {
class WavPlayer final : public Source<1> {
Wav wav_;
std::size_t position_;
bool looping_;
public:
WavPlayer();
WavPlayer(const WavPlayer& rhs);
WavPlayer(WavPlayer&& rhs) noexcept;
WavPlayer& operator=(const WavPlayer& rhs);
WavPlayer& operator=(WavPlayer&& rhs) noexcept;
WavPlayer(Wav&& wav) noexcept;
virtual std::size_t generate(tcb::span<Sample<1>> buffer) override;
bool looping() const { return looping_; }
void looping(bool looping) { looping_ = looping; }
std::size_t sample_rate() const { return wav_.sample_rate(); }
float duration_seconds() const { return wav_.length() / static_cast<float>(wav_.sample_rate()); }
void seek(float seconds) { position_ = seconds * wav_.sample_rate(); }
};
} // namespace srb2::audio
#endif // __SRB2_AUDIO_WAV_PLAYER_HPP__

167
src/audio/xmp.cpp Normal file
View file

@ -0,0 +1,167 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "xmp.hpp"
#include <limits>
#include "../cxxutil.hpp"
using namespace srb2;
using namespace srb2::audio;
XmpException::XmpException(int code) : code_(code) {
}
const char* XmpException::what() const noexcept {
switch (code_) {
case -XMP_ERROR_INTERNAL:
return "XMP_ERROR_INTERNAL";
case -XMP_ERROR_FORMAT:
return "XMP_ERROR_FORMAT";
case -XMP_ERROR_LOAD:
return "XMP_ERROR_LOAD";
case -XMP_ERROR_DEPACK:
return "XMP_ERROR_DEPACK";
case -XMP_ERROR_SYSTEM:
return "XMP_ERROR_SYSTEM";
case -XMP_ERROR_INVALID:
return "XMP_ERROR_INVALID";
case -XMP_ERROR_STATE:
return "XMP_ERROR_STATE";
default:
return "unknown";
}
}
template <size_t C>
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) {
_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) {
_init();
}
template <size_t 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_);
std::swap(looping_, rhs.looping_);
}
template <size_t C>
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_);
std::swap(looping_, rhs.looping_);
return *this;
};
template <size_t C>
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) {
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
int result = xmp_play_buffer(instance_, buffer.data(), buffer.size_bytes(), !looping_);
if (result == -XMP_END)
return 0;
if (result != 0)
throw XmpException(result);
return buffer.size();
}
template <size_t C>
void Xmp<C>::reset() {
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
xmp_restart_module(instance_);
}
template <size_t C>
float Xmp<C>::duration_seconds() const {
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
xmp_frame_info info;
xmp_get_frame_info(instance_, &info);
return static_cast<float>(info.total_time) / 1000.f;
}
template <size_t C>
void Xmp<C>::seek(int position_ms) {
SRB2_ASSERT(instance_ != nullptr);
SRB2_ASSERT(module_loaded_ == true);
int err = xmp_seek_time(instance_, position_ms);
if (err != 0)
throw XmpException(err);
}
template <size_t C>
void Xmp<C>::_init() {
if (instance_)
return;
if (data_.size() >= std::numeric_limits<long>::max())
throw std::logic_error("Buffer is too large for xmp");
if (data_.size() == 0)
throw std::logic_error("Insufficient data from stream");
instance_ = xmp_create_context();
if (instance_ == nullptr) {
throw std::bad_alloc();
}
int result = xmp_load_module_from_memory(instance_, data_.data(), data_.size());
if (result != 0) {
xmp_free_context(instance_);
instance_ = nullptr;
throw XmpException(result);
}
module_loaded_ = true;
int flags = 0;
if constexpr (C == 1) {
flags |= XMP_FORMAT_MONO;
}
result = xmp_start_player(instance_, 44100, flags);
if (result != 0) {
xmp_release_module(instance_);
module_loaded_ = false;
xmp_free_context(instance_);
instance_ = nullptr;
throw XmpException(result);
}
}
template class srb2::audio::Xmp<1>;
template class srb2::audio::Xmp<2>;

78
src/audio/xmp.hpp Normal file
View file

@ -0,0 +1,78 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_XMP_HPP__
#define __SRB2_AUDIO_XMP_HPP__
#include <array>
#include <cstddef>
#include <exception>
#include <stdexcept>
#include <utility>
#include <vector>
#include <tcb/span.hpp>
#include <xmp.h>
#include "../io/streams.hpp"
namespace srb2::audio {
class XmpException : public std::exception {
int code_;
public:
XmpException(int code);
virtual const char* what() const noexcept override final;
};
template <size_t C>
class Xmp final {
std::vector<std::byte> data_;
xmp_context instance_;
bool module_loaded_;
bool looping_;
public:
Xmp();
explicit Xmp(std::vector<std::byte> data);
explicit Xmp(tcb::span<std::byte> data);
Xmp(const Xmp<C>&) = delete;
Xmp(Xmp<C>&& rhs) noexcept;
Xmp& operator=(const Xmp&) = delete;
Xmp& operator=(Xmp&& rhs) noexcept;
std::size_t play_buffer(tcb::span<std::array<int16_t, C>> buffer);
bool looping() const { return looping_; };
void looping(bool looping) { looping_ = looping; };
void reset();
float duration_seconds() const;
void seek(int position_ms);
~Xmp();
private:
void _init();
};
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) {
std::vector<std::byte> data = srb2::io::read_to_vec(stream);
return Xmp<C> {std::move(data)};
}
} // namespace srb2::audio
#endif // __SRB2_AUDIO_XMP_HPP__

57
src/audio/xmp_player.cpp Normal file
View file

@ -0,0 +1,57 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "xmp_player.hpp"
#include <cmath>
using namespace srb2;
using namespace srb2::audio;
template <size_t C>
XmpPlayer<C>::XmpPlayer(Xmp<C>&& xmp) : xmp_(std::move(xmp)), buf_() {
}
template <size_t C>
XmpPlayer<C>::XmpPlayer(XmpPlayer&& rhs) noexcept = default;
template <size_t C>
XmpPlayer<C>& XmpPlayer<C>::operator=(XmpPlayer<C>&& rhs) noexcept = default;
template <size_t C>
XmpPlayer<C>::~XmpPlayer() = default;
template <size_t C>
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++) {
buffer[i].amplitudes[j] = buf_[i][j] / 32768.f;
}
}
return ret;
}
template <size_t C>
float XmpPlayer<C>::duration_seconds() const {
return xmp_.duration_seconds();
}
template <size_t C>
void XmpPlayer<C>::seek(float position_seconds) {
xmp_.seek(static_cast<int>(std::round(position_seconds * 1000.f)));
}
template class srb2::audio::XmpPlayer<1>;
template class srb2::audio::XmpPlayer<2>;

48
src/audio/xmp_player.hpp Normal file
View file

@ -0,0 +1,48 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_AUDIO_XMP_PLAYER_HPP__
#define __SRB2_AUDIO_XMP_PLAYER_HPP__
#include "source.hpp"
#include "xmp.hpp"
namespace srb2::audio {
template <size_t C>
class XmpPlayer final : public Source<C> {
Xmp<C> xmp_;
std::vector<std::array<int16_t, C>> buf_;
public:
XmpPlayer(Xmp<C>&& xmp);
XmpPlayer(const XmpPlayer<C>&) = delete;
XmpPlayer(XmpPlayer<C>&& rhs) noexcept;
XmpPlayer<C>& operator=(const XmpPlayer<C>&) = delete;
XmpPlayer<C>& operator=(XmpPlayer<C>&& rhs) noexcept;
~XmpPlayer();
virtual std::size_t generate(tcb::span<Sample<C>> buffer) override final;
bool looping() { return xmp_.looping(); };
void looping(bool looping) { xmp_.looping(looping); }
void reset() { xmp_.reset(); }
float duration_seconds() const;
void seek(float position_seconds);
};
extern template class XmpPlayer<1>;
extern template class XmpPlayer<2>;
} // namespace srb2::audio
#endif // __SRB2_AUDIO_XMP_PLAYER_HPP__

View file

@ -11,6 +11,8 @@
#include "config.h"
const char *compbranch = SRB2_COMP_BRANCH;
const char *comprevision = SRB2_COMP_REVISION;
const char *comptype = CMAKE_BUILD_TYPE;
const int compoptimized = SRB2_COMP_OPTIMIZED;
#elif (defined(COMPVERSION))
#include "comptime.h"

View file

@ -20,6 +20,9 @@
#define COMPVERSION_UNCOMMITTED
#endif
#define CMAKE_BUILD_TYPE "${CMAKE_BUILD_TYPE}"
#cmakedefine01 SRB2_COMP_OPTIMIZED
#endif
/* Manually defined asset hashes for non-CMake builds

View file

@ -358,7 +358,22 @@ static void CON_SetupColormaps(void)
*memorysrc = (UINT8)(i & 0xFF); // remap each color to itself...
purplemap[0] = (UINT8)163;
yellowmap[0] = (UINT8)73;
yellowmap[1] = (UINT8)73;
yellowmap[3] = (UINT8)74;
yellowmap[6] = (UINT8)74;
yellowmap[7] = (UINT8)190;
yellowmap[8] = (UINT8)190;
yellowmap[10] = (UINT8)190;
yellowmap[12] = (UINT8)190;
yellowmap[14] = (UINT8)149;
yellowmap[15] = (UINT8)149;
yellowmap[16] = (UINT8)149;
yellowmap[21] = (UINT8)152;
yellowmap[23] = (UINT8)173;
yellowmap[24] = (UINT8)167;
greenmap[0] = (UINT8)98;
bluemap[0] = (UINT8)148;
redmap[0] = (UINT8)34; // battle

View file

@ -54,7 +54,6 @@
#include "k_pwrlv.h"
#include "k_bot.h"
#include "k_grandprix.h"
#include "k_boss.h"
#include "doomstat.h"
#include "s_sound.h" // sfx_syfail
#include "m_cond.h" // netUnlocked
@ -899,9 +898,6 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
UINT8 *p;
size_t mirror_length;
const char *httpurl = cv_httpsource.string;
UINT8 prefgametype = (cv_kartgametypepreference.value == -1)
? gametype
: cv_kartgametypepreference.value;
netbuffer->packettype = PT_SERVERINFO;
netbuffer->u.serverinfo._255 = 255;
@ -933,7 +929,7 @@ static void SV_SendServerInfo(INT32 node, tic_t servertime)
else
netbuffer->u.serverinfo.refusereason = 0;
strncpy(netbuffer->u.serverinfo.gametypename, Gametype_Names[prefgametype],
strncpy(netbuffer->u.serverinfo.gametypename, gametypes[gametype]->name,
sizeof netbuffer->u.serverinfo.gametypename);
netbuffer->u.serverinfo.modifiedgame = (UINT8)modifiedgame;
netbuffer->u.serverinfo.cheatsenabled = CV_CheatsEnabled();
@ -1141,35 +1137,32 @@ static boolean SV_ResendingSavegameToAnyone(void)
static void SV_SendSaveGame(INT32 node, boolean resending)
{
size_t length, compressedlen;
savebuffer_t save;
savebuffer_t save = {0};
UINT8 *compressedsave;
UINT8 *buffertosend;
// first save it in a malloced buffer
save.size = NETSAVEGAMESIZE;
save.buffer = (UINT8 *)malloc(save.size);
if (!save.buffer)
if (P_SaveBufferAlloc(&save, NETSAVEGAMESIZE) == false)
{
CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
return;
}
// Leave room for the uncompressed length.
save.p = save.buffer + sizeof(UINT32);
save.end = save.buffer + save.size;
save.p += sizeof(UINT32);
P_SaveNetGame(&save, resending);
length = save.p - save.buffer;
if (length > NETSAVEGAMESIZE)
{
free(save.buffer);
P_SaveBufferFree(&save);
I_Error("Savegame buffer overrun");
}
// Allocate space for compressed save: one byte fewer than for the
// uncompressed data to ensure that the compression is worthwhile.
compressedsave = malloc(length - 1);
compressedsave = Z_Malloc(length - 1, PU_STATIC, NULL);
if (!compressedsave)
{
CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
@ -1180,7 +1173,7 @@ static void SV_SendSaveGame(INT32 node, boolean resending)
if ((compressedlen = lzf_compress(save.buffer + sizeof(UINT32), length - sizeof(UINT32), compressedsave + sizeof(UINT32), length - sizeof(UINT32) - 1)))
{
// Compressing succeeded; send compressed data
free(save.buffer);
P_SaveBufferFree(&save);
// State that we're compressed.
buffertosend = compressedsave;
@ -1190,14 +1183,14 @@ static void SV_SendSaveGame(INT32 node, boolean resending)
else
{
// Compression failed to make it smaller; send original
free(compressedsave);
Z_Free(compressedsave);
// State that we're not compressed
buffertosend = save.buffer;
WRITEUINT32(save.buffer, 0);
}
AddRamToSendQueue(node, buffertosend, length, SF_RAM, 0);
AddRamToSendQueue(node, buffertosend, length, SF_Z_RAM, 0);
// Remember when we started sending the savegame so we can handle timeouts
sendingsavegame[node] = true;
@ -1211,7 +1204,7 @@ static consvar_t cv_dumpconsistency = CVAR_INIT ("dumpconsistency", "Off", CV_SA
static void SV_SavedGame(void)
{
size_t length;
savebuffer_t save;
savebuffer_t save = {0};
char tmpsave[256];
if (!cv_dumpconsistency.value)
@ -1220,22 +1213,18 @@ static void SV_SavedGame(void)
sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
// first save it in a malloced buffer
save.size = NETSAVEGAMESIZE;
save.p = save.buffer = (UINT8 *)malloc(save.size);
if (!save.p)
if (P_SaveBufferAlloc(&save, NETSAVEGAMESIZE) == false)
{
CONS_Alert(CONS_ERROR, M_GetText("No more free memory for savegame\n"));
return;
}
save.end = save.buffer + save.size;
P_SaveNetGame(&save, false);
length = save.p - save.buffer;
if (length > NETSAVEGAMESIZE)
{
free(save.buffer);
P_SaveBufferFree(&save);
I_Error("Savegame buffer overrun");
}
@ -1243,7 +1232,7 @@ static void SV_SavedGame(void)
if (!FIL_WriteFile(tmpsave, save.buffer, length))
CONS_Printf(M_GetText("Didn't save %s for netgame"), tmpsave);
free(save.buffer);
P_SaveBufferFree(&save);
}
#undef TMPSAVENAME
@ -1253,37 +1242,31 @@ static void SV_SavedGame(void)
static void CL_LoadReceivedSavegame(boolean reloading)
{
savebuffer_t save;
savebuffer_t save = {0};
size_t length, decompressedlen;
char tmpsave[256];
sprintf(tmpsave, "%s" PATHSEP TMPSAVENAME, srb2home);
length = FIL_ReadFile(tmpsave, &save.buffer);
CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
if (!length)
if (P_SaveBufferFromFile(&save, tmpsave) == false)
{
I_Error("Can't read savegame sent");
return;
}
save.p = save.buffer;
save.size = length;
save.end = save.buffer + save.size;
length = save.size;
CONS_Printf(M_GetText("Loading savegame length %s\n"), sizeu1(length));
// Decompress saved game if necessary.
decompressedlen = READUINT32(save.p);
if(decompressedlen > 0)
if (decompressedlen > 0)
{
UINT8 *decompressedbuffer = Z_Malloc(decompressedlen, PU_STATIC, NULL);
lzf_decompress(save.p, length - sizeof(UINT32), decompressedbuffer, decompressedlen);
Z_Free(save.buffer);
save.p = save.buffer = decompressedbuffer;
save.size = decompressedlen;
save.end = save.buffer + decompressedlen;
P_SaveBufferFree(&save);
P_SaveBufferFromExisting(&save, decompressedbuffer, decompressedlen);
}
paused = false;
@ -1315,10 +1298,13 @@ static void CL_LoadReceivedSavegame(boolean reloading)
}
// done
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
if (unlink(tmpsave) == -1)
{
CONS_Alert(CONS_ERROR, M_GetText("Can't delete %s\n"), tmpsave);
}
consistancy[gametic%BACKUPTICS] = Consistancy();
CON_ToggleOff();
@ -2087,8 +2073,9 @@ static void CL_ConnectToServer(void)
Y_EndVote();
DEBFILE(va("waiting %d nodes\n", doomcom->numnodes));
M_ClearMenus(true);
G_SetGamestate(GS_WAITINGPLAYERS);
if (wipegamestate == GS_MENU)
M_ClearMenus(true);
wipegamestate = GS_WAITINGPLAYERS;
ClearAdminPlayers();
@ -3754,6 +3741,9 @@ static void Got_AddBot(UINT8 **p, INT32 playernum)
sprintf(player_names[newplayernum], "%s", skins[skinnum].realname);
SetPlayerSkinByNum(newplayernum, skinnum);
players[newplayernum].spectator = !(gametyperules & GTR_BOTS)
|| (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE);
if (netgame)
{
HU_AddChatText(va("\x82*Bot %d has been added to the game", newplayernum+1), false);
@ -6067,7 +6057,7 @@ void CL_ClearRewinds(void)
rewind_t *CL_SaveRewindPoint(size_t demopos)
{
savebuffer_t save;
savebuffer_t save = {0};
rewind_t *rewind;
if (rewindhead && rewindhead->leveltime + REWIND_POINT_INTERVAL > leveltime)
@ -6077,10 +6067,7 @@ rewind_t *CL_SaveRewindPoint(size_t demopos)
if (!rewind)
return NULL;
save.buffer = save.p = rewind->savebuffer;
save.size = NETSAVEGAMESIZE;
save.end = save.buffer + save.size;
P_SaveBufferFromExisting(&save, rewind->savebuffer, NETSAVEGAMESIZE);
P_SaveNetGame(&save, false);
rewind->leveltime = leveltime;
@ -6093,7 +6080,7 @@ rewind_t *CL_SaveRewindPoint(size_t demopos)
rewind_t *CL_RewindToTime(tic_t time)
{
savebuffer_t save;
savebuffer_t save = {0};
rewind_t *rewind;
while (rewindhead && rewindhead->leveltime > time)
@ -6106,10 +6093,7 @@ rewind_t *CL_RewindToTime(tic_t time)
if (!rewindhead)
return NULL;
save.buffer = save.p = rewindhead->savebuffer;
save.size = NETSAVEGAMESIZE;
save.end = save.buffer + save.size;
P_SaveBufferFromExisting(&save, rewindhead->savebuffer, NETSAVEGAMESIZE);
P_LoadNetGame(&save, false);
wipegamestate = gamestate; // No fading back in!

View file

@ -72,7 +72,6 @@
// SRB2Kart
#include "k_grandprix.h"
#include "k_boss.h"
#include "doomstat.h"
#include "m_random.h" // P_ClearRandom
#include "k_specialstage.h"
@ -140,7 +139,8 @@ char srb2home[256] = ".";
char srb2path[256] = ".";
boolean usehome = true;
const char *pandf = "%s" PATHSEP "%s";
static char addonsdir[MAX_WADPATH];
char addonsdir[MAX_WADPATH];
char downloaddir[sizeof addonsdir + sizeof DOWNLOADDIR_PART] = "DOWNLOAD";
//
// EVENT HANDLING
@ -964,12 +964,6 @@ void D_StartTitle(void)
// Reset GP
memset(&grandprixinfo, 0, sizeof(struct grandprixinfo));
// Reset boss info
K_ResetBossInfo();
// Reset Special Stage
K_ResetSpecialStage();
// empty maptol so mario/etc sounds don't play in sound test when they shouldn't
maptol = 0;
@ -1201,13 +1195,14 @@ D_ConvertVersionNumbers (void)
//
void D_SRB2Main(void)
{
INT32 i, p;
INT32 i, j, p;
#ifdef DEVELOP
INT32 pstartmap = 1; // default to first loaded map (Test Run)
#else
INT32 pstartmap = 0; // default to random map (0 is not a valid map number)
#endif
boolean autostart = false;
INT32 newgametype = -1;
/* break the version string into version numbers, for netplay */
D_ConvertVersionNumbers();
@ -1368,7 +1363,7 @@ void D_SRB2Main(void)
/* and downloads in a subdirectory */
snprintf(downloaddir, sizeof downloaddir, "%s%s%s",
addonsdir, PATHSEP, "downloads");
addonsdir, PATHSEP, DOWNLOADDIR_PART);
// rand() needs seeded regardless of password
srand((unsigned int)time(NULL));
@ -1520,11 +1515,6 @@ void D_SRB2Main(void)
CON_SetLoadingProgress(LOADED_HUINIT);
memset(timelimits, 0, sizeof(timelimits));
memset(pointlimits, 0, sizeof(pointlimits));
timelimits[GT_BATTLE] = 2;
D_RegisterServerCommands();
D_RegisterClientCommands(); // be sure that this is called before D_CheckNetGame
R_RegisterEngineStuff();
@ -1532,14 +1522,14 @@ void D_SRB2Main(void)
I_RegisterSysCommands();
M_Init();
//--------------------------------------------------------- CONFIG.CFG
M_FirstLoadConfig(); // WARNING : this do a "COM_BufExecute()"
// Load Profiles now that default controls have been defined
PR_LoadProfiles(); // load control profiles
M_Init();
#if (defined (__unix__) && !defined (MSDOS)) || defined (UNIXCOMMON) || defined (HAVE_SDL)
VID_PrepareModeList(); // Regenerate Modelist according to cv_fullscreen
#endif
@ -1801,8 +1791,6 @@ void D_SRB2Main(void)
if (M_CheckParm("-gametype") && M_IsNextParm())
{
// from Command_Map_f
INT32 j;
INT16 newgametype = -1;
const char *sgametype = M_GetNextParm();
newgametype = G_GetGametypeByName(sgametype);
@ -1810,7 +1798,7 @@ void D_SRB2Main(void)
if (newgametype == -1) // reached end of the list with no match
{
j = atoi(sgametype); // assume they gave us a gametype number, which is okay too
if (j >= 0 && j < gametypecount)
if (j >= 0 && j < numgametypes)
newgametype = (INT16)j;
}
@ -1824,7 +1812,6 @@ void D_SRB2Main(void)
if (M_CheckParm("-skill") && M_IsNextParm())
{
INT32 j;
INT16 newskill = -1;
const char *sskill = M_GetNextParm();
@ -1877,11 +1864,20 @@ void D_SRB2Main(void)
if (grandprixinfo.gp == true && mapheaderinfo[pstartmap-1])
{
if (mapheaderinfo[pstartmap-1]->typeoflevel & TOL_SPECIAL)
if (newgametype == -1)
{
specialStage.active = true;
specialStage.encore = grandprixinfo.encore;
grandprixinfo.eventmode = GPEVENT_SPECIAL;
newgametype = G_GuessGametypeByTOL(mapheaderinfo[pstartmap-1]->typeoflevel);
if (newgametype != -1)
{
j = gametype;
G_SetGametype(newgametype);
D_GameTypeChanged(j);
}
if (gametyperules & (GTR_BOSS|GTR_CATCHER))
grandprixinfo.eventmode = GPEVENT_SPECIAL;
else if (gametype != GT_RACE)
grandprixinfo.eventmode = GPEVENT_BONUS;
}
G_SetUsedCheats();

View file

@ -29,6 +29,7 @@ extern char srb2home[256]; //Alam: My Home
extern boolean usehome; //Alam: which path?
extern const char *pandf; //Alam: how to path?
extern char srb2path[256]; //Alam: SRB2's Home
extern char addonsdir[MAX_WADPATH]; // Where addons are stored
// the infinite loop of D_SRB2Loop() called from win_main for windows version
void D_SRB2Loop(void) FUNCNORETURN;

View file

@ -57,7 +57,6 @@
#include "k_color.h"
#include "k_respawn.h"
#include "k_grandprix.h"
#include "k_boss.h"
#include "k_follower.h"
#include "doomstat.h"
#include "deh_tables.h"
@ -399,10 +398,6 @@ consvar_t cv_kartbumpers = CVAR_INIT ("battlebumpers", "3", CV_NETVAR, kartbumpe
consvar_t cv_kartfrantic = CVAR_INIT ("franticitems", "Off", CV_NETVAR|CV_CALL|CV_NOINIT, CV_OnOff, KartFrantic_OnChange);
static CV_PossibleValue_t kartencore_cons_t[] = {{-1, "Auto"}, {0, "Off"}, {1, "On"}, {0, NULL}};
consvar_t cv_kartencore = CVAR_INIT ("encore", "Auto", CV_NETVAR|CV_CALL|CV_NOINIT, kartencore_cons_t, KartEncore_OnChange);
static CV_PossibleValue_t kartvoterulechanges_cons_t[] = {{0, "Never"}, {1, "Sometimes"}, {2, "Frequent"}, {3, "Always"}, {0, NULL}};
consvar_t cv_kartvoterulechanges = CVAR_INIT ("voterulechanges", "Frequent", CV_NETVAR, kartvoterulechanges_cons_t, NULL);
static CV_PossibleValue_t kartgametypepreference_cons_t[] = {{-1, "None"}, {GT_RACE, "Race"}, {GT_BATTLE, "Battle"}, {0, NULL}};
consvar_t cv_kartgametypepreference = CVAR_INIT ("gametypepreference", "None", CV_NETVAR, kartgametypepreference_cons_t, NULL);
static CV_PossibleValue_t kartspeedometer_cons_t[] = {{0, "Off"}, {1, "Percentage"}, {2, "Kilometers"}, {3, "Miles"}, {4, "Fracunits"}, {0, NULL}};
consvar_t cv_kartspeedometer = CVAR_INIT ("speedometer", "Percentage", CV_SAVE, kartspeedometer_cons_t, NULL); // use tics in display
static CV_PossibleValue_t kartvoices_cons_t[] = {{0, "Never"}, {1, "Tasteful"}, {2, "Meme"}, {0, NULL}};
@ -485,10 +480,6 @@ consvar_t cv_timelimit = CVAR_INIT ("timelimit", "None", CV_NETVAR|CV_CALL|CV_NO
static CV_PossibleValue_t numlaps_cons_t[] = {{1, "MIN"}, {MAX_LAPS, "MAX"}, {0, "Map default"}, {0, NULL}};
consvar_t cv_numlaps = CVAR_INIT ("numlaps", "Map default", CV_SAVE|CV_NETVAR|CV_CALL|CV_CHEAT, numlaps_cons_t, NumLaps_OnChange);
// Point and time limits for every gametype
INT32 pointlimits[NUMGAMETYPES];
INT32 timelimits[NUMGAMETYPES];
consvar_t cv_forceskin = CVAR_INIT ("forceskin", "None", CV_NETVAR|CV_CALL|CV_CHEAT, NULL, ForceSkin_OnChange);
consvar_t cv_downloading = CVAR_INIT ("downloading", "On", 0, CV_OnOff, NULL);
@ -557,13 +548,11 @@ char timedemo_csv_id[256];
boolean timedemo_quit;
INT16 gametype = GT_RACE;
UINT32 gametyperules = 0;
INT16 gametypecount = GT_FIRSTFREESLOT;
INT16 numgametypes = GT_FIRSTFREESLOT;
boolean forceresetplayers = false;
boolean deferencoremode = false;
UINT8 splitscreen = 0;
boolean circuitmap = false;
INT32 adminplayers[MAXPLAYERS];
// Scheduled commands.
@ -2530,19 +2519,11 @@ void D_MapChange(INT32 mapnum, INT32 newgametype, boolean pencoremode, boolean r
CONS_Debug(DBG_GAMELOGIC, "Map change: mapnum=%d gametype=%d pencoremode=%d resetplayers=%d delay=%d skipprecutscene=%d\n",
mapnum, newgametype, pencoremode, resetplayers, delay, skipprecutscene);
if ((netgame || multiplayer) && !((gametype == newgametype) && (gametypedefaultrules[newgametype] & GTR_CAMPAIGN)))
if ((netgame || multiplayer) && (grandprixinfo.gp != false))
FLS = false;
// Too lazy to change the input value for every instance of this function.......
if (bossinfo.boss == true)
{
pencoremode = bossinfo.encore;
}
else if (specialStage.active == true)
{
pencoremode = specialStage.encore;
}
else if (grandprixinfo.gp == true)
if (grandprixinfo.gp == true)
{
pencoremode = grandprixinfo.encore;
}
@ -2596,14 +2577,13 @@ void D_SetupVote(void)
UINT8 buf[5*2]; // four UINT16 maps (at twice the width of a UINT8), and two gametypes
UINT8 *p = buf;
INT32 i;
UINT8 gt = (cv_kartgametypepreference.value == -1) ? gametype : cv_kartgametypepreference.value;
UINT8 secondgt = G_SometimesGetDifferentGametype(gt);
UINT8 secondgt = G_SometimesGetDifferentGametype();
INT16 votebuffer[4] = {-1,-1,-1,0};
if ((cv_kartencore.value == 1) && (gametypedefaultrules[gt] & GTR_CIRCUIT))
WRITEUINT8(p, (gt|VOTEMODIFIER_ENCORE));
if ((cv_kartencore.value == 1) && (gametyperules & GTR_ENCORE))
WRITEUINT8(p, (gametype|VOTEMODIFIER_ENCORE));
else
WRITEUINT8(p, gt);
WRITEUINT8(p, gametype);
WRITEUINT8(p, secondgt);
secondgt &= ~VOTEMODIFIER_ENCORE;
@ -2613,9 +2593,9 @@ void D_SetupVote(void)
if (i == 2) // sometimes a different gametype
m = G_RandMap(G_TOLFlag(secondgt), prevmap, ((secondgt != gametype) ? 2 : 0), 0, true, votebuffer);
else if (i >= 3) // unknown-random and formerly force-unknown MAP HELL
m = G_RandMap(G_TOLFlag(gt), prevmap, 0, (i-2), (i < 4), votebuffer);
m = G_RandMap(G_TOLFlag(gametype), prevmap, 0, (i-2), (i < 4), votebuffer);
else
m = G_RandMap(G_TOLFlag(gt), prevmap, 0, 0, true, votebuffer);
m = G_RandMap(G_TOLFlag(gametype), prevmap, 0, 0, true, votebuffer);
if (i < 3)
votebuffer[i] = m;
WRITEUINT16(p, m);
@ -2758,13 +2738,7 @@ static void Command_Map_f(void)
if (option_gametype)
{
if (!multiplayer)
{
CONS_Printf(M_GetText(
"You can't switch gametypes in single player!\n"));
return;
}
else if (COM_Argc() < option_gametype + 2)/* no argument after? */
if (COM_Argc() < option_gametype + 2)/* no argument after? */
{
CONS_Alert(CONS_ERROR,
"No gametype name follows parameter '%s'.\n",
@ -2822,7 +2796,7 @@ static void Command_Map_f(void)
if (isdigit(gametypename[0]))
{
d = atoi(gametypename);
if (d >= 0 && d < gametypecount)
if (d >= 0 && d < numgametypes)
newgametype = d;
else
{
@ -2830,7 +2804,7 @@ static void Command_Map_f(void)
"Gametype number %d is out of range. Use a number between"
" 0 and %d inclusive. ...Or just use the name. :v\n",
d,
gametypecount-1);
numgametypes-1);
Z_Free(realmapname);
Z_Free(mapname);
return;
@ -2839,13 +2813,23 @@ static void Command_Map_f(void)
else
{
CONS_Alert(CONS_ERROR,
"'%s' is not a gametype.\n",
"'%s' is not a valid gametype.\n",
gametypename);
Z_Free(realmapname);
Z_Free(mapname);
return;
}
}
if (Playing() && netgame && (gametypes[newgametype]->rules & GTR_FORBIDMP))
{
CONS_Alert(CONS_ERROR,
"'%s' is not a net-compatible gametype.\n",
gametypename);
Z_Free(realmapname);
Z_Free(mapname);
return;
}
}
else if (!Playing())
{
@ -2853,7 +2837,15 @@ static void Command_Map_f(void)
if (mapheaderinfo[newmapnum-1])
{
// Let's just guess so we don't have to specify the gametype EVERY time...
newgametype = (mapheaderinfo[newmapnum-1]->typeoflevel & (TOL_BATTLE|TOL_BOSS)) ? GT_BATTLE : GT_RACE;
newgametype = G_GuessGametypeByTOL(mapheaderinfo[newmapnum-1]->typeoflevel);
if (newgametype == -1)
{
CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support any known gametype!\n"), realmapname, G_BuildMapName(newmapnum));
Z_Free(realmapname);
Z_Free(mapname);
return;
}
}
}
@ -2865,6 +2857,8 @@ static void Command_Map_f(void)
if (!M_SecretUnlocked(SECRET_ENCORE, false) && newencoremode == true && !usingcheats)
{
CONS_Alert(CONS_NOTICE, M_GetText("You haven't unlocked Encore Mode yet!\n"));
Z_Free(realmapname);
Z_Free(mapname);
return;
}
}
@ -2885,8 +2879,7 @@ static void Command_Map_f(void)
mapheaderinfo[newmapnum-1]->typeoflevel & G_TOLFlag(newgametype)
))
{
CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum),
(multiplayer ? gametype_cons_t[newgametype].strvalue : "Single Player"));
CONS_Alert(CONS_WARNING, M_GetText("%s (%s) doesn't support %s mode!\n(Use -force to override)\n"), realmapname, G_BuildMapName(newmapnum), gametypes[newgametype]->name);
Z_Free(realmapname);
Z_Free(mapname);
return;
@ -2895,8 +2888,7 @@ static void Command_Map_f(void)
{
fromlevelselect =
( netgame || multiplayer ) &&
newgametype == gametype &&
gametypedefaultrules[newgametype] & GTR_CAMPAIGN;
grandprixinfo.gp != false;
}
}
@ -2952,35 +2944,18 @@ static void Command_Map_f(void)
grandprixinfo.eventmode = GPEVENT_NONE;
if (newgametype == GT_BATTLE)
if (gametypes[newgametype]->rules & (GTR_BOSS|GTR_CATCHER))
{
grandprixinfo.eventmode = GPEVENT_SPECIAL;
}
else if (newgametype != GT_RACE)
{
grandprixinfo.eventmode = GPEVENT_BONUS;
if (mapheaderinfo[newmapnum-1] &&
mapheaderinfo[newmapnum-1]->typeoflevel & TOL_BOSS)
{
bossinfo.boss = true;
bossinfo.encore = newencoremode;
}
else
{
bossinfo.boss = false;
K_ResetBossInfo();
}
}
else
if (!Playing())
{
if (mapheaderinfo[newmapnum-1] &&
mapheaderinfo[newmapnum-1]->typeoflevel & TOL_SPECIAL) // Special Stage
{
specialStage.active = true;
specialStage.encore = newencoremode;
grandprixinfo.eventmode = GPEVENT_SPECIAL;
}
else
{
specialStage.active = false;
}
multiplayer = true;
}
}
@ -3029,12 +3004,12 @@ static void Got_Mapcmd(UINT8 **cp, INT32 playernum)
gametype = READUINT8(*cp);
G_SetGametype(gametype); // I fear putting that macro as an argument
if (gametype < 0 || gametype >= gametypecount)
if (gametype < 0 || gametype >= numgametypes)
gametype = lastgametype;
else if (gametype != lastgametype)
D_GameTypeChanged(lastgametype); // emulate consvar_t behavior for gametype
if (!(gametyperules & GTR_CIRCUIT) && !bossinfo.boss)
if (!(gametyperules & GTR_ENCORE))
pencoremode = false;
skipprecutscene = ((flags & (1<<2)) != 0);
@ -3086,9 +3061,10 @@ static void Command_RandomMap(void)
{
INT32 oldmapnum;
INT32 newmapnum;
INT32 newgametype;
boolean newencoremode;
INT32 newgametype = (Playing() ? gametype : menugametype);
boolean newencore = false;
boolean newresetplayers;
size_t option_gametype;
if (client && !IsPlayerAdmin(consoleplayer))
{
@ -3096,13 +3072,69 @@ static void Command_RandomMap(void)
return;
}
if ((option_gametype = COM_CheckPartialParm("-g")))
{
const char *gametypename;
if (COM_Argc() < option_gametype + 2)/* no argument after? */
{
CONS_Alert(CONS_ERROR,
"No gametype name follows parameter '%s'.\n",
COM_Argv(option_gametype));
return;
}
// new gametype value
// use current one by default
gametypename = COM_Argv(option_gametype + 1);
newgametype = G_GetGametypeByName(gametypename);
if (newgametype == -1) // reached end of the list with no match
{
/* Did they give us a gametype number? That's okay too! */
if (isdigit(gametypename[0]))
{
INT16 d = atoi(gametypename);
if (d >= 0 && d < numgametypes)
newgametype = d;
else
{
CONS_Alert(CONS_ERROR,
"Gametype number %d is out of range. Use a number between"
" 0 and %d inclusive. ...Or just use the name. :v\n",
d,
numgametypes-1);
return;
}
}
else
{
CONS_Alert(CONS_ERROR,
"'%s' is not a valid gametype.\n",
gametypename);
return;
}
}
if (Playing() && netgame && (gametypes[newgametype]->rules & GTR_FORBIDMP))
{
CONS_Alert(CONS_ERROR,
"'%s' is not a net-compatible gametype.\n",
gametypename);
return;
}
}
// TODO: Handle singleplayer conditions.
// The existing ones are way too annoyingly complicated and "anti-cheat" for my tastes.
if (Playing())
{
newgametype = gametype;
newencoremode = encoremode;
if (cv_kartencore.value == 1 && (gametypes[newgametype]->rules & GTR_ENCORE))
{
newencore = true;
}
newresetplayers = false;
if (gamestate == GS_LEVEL)
@ -3116,14 +3148,12 @@ static void Command_RandomMap(void)
}
else
{
newgametype = cv_dummygametype.value; // Changed from cv_newgametype to match newmenus
newencoremode = false;
newresetplayers = true;
oldmapnum = -1;
}
newmapnum = G_RandMap(G_TOLFlag(newgametype), oldmapnum, 0, 0, false, NULL) + 1;
D_MapChange(newmapnum, newgametype, newencoremode, newresetplayers, 0, false, false);
D_MapChange(newmapnum, newgametype, newencore, newresetplayers, 0, false, false);
}
static void Command_RestartLevel(void)
@ -3742,7 +3772,7 @@ static void Got_Teamchange(UINT8 **cp, INT32 playernum)
// Clear player score and rings if a spectator.
if (players[playernum].spectator)
{
if (gametyperules & GTR_BUMPERS) // SRB2kart
if (gametyperules & GTR_POINTLIMIT) // SRB2kart
{
players[playernum].roundscore = 0;
K_CalculateBattleWanted();
@ -4774,16 +4804,13 @@ static void Command_Version_f(void)
else // 16-bit? 128-bit?
CONS_Printf("Bits Unknown ");
CONS_Printf("%s ", comptype);
// No ASM?
#ifdef NOASM
CONS_Printf("\x85" "NOASM " "\x80");
#endif
// Debug build
#ifdef _DEBUG
CONS_Printf("\x85" "DEBUG " "\x80");
#endif
// DEVELOP build
#if defined(TESTERS)
CONS_Printf("\x88" "TESTERS " "\x80");
@ -4812,15 +4839,9 @@ static void Command_ShowGametype_f(void)
{
const char *gametypestr = NULL;
if (!(netgame || multiplayer)) // print "Single player" instead of "Race"
{
CONS_Printf(M_GetText("Current gametype is %s\n"), "Single Player");
return;
}
// get name string for current gametype
if (gametype >= 0 && gametype < gametypecount)
gametypestr = Gametype_Names[gametype];
if (gametype >= 0 && gametype < numgametypes)
gametypestr = gametypes[gametype]->name;
if (gametypestr)
CONS_Printf(M_GetText("Current gametype is %s\n"), gametypestr);
@ -4999,10 +5020,10 @@ void D_GameTypeChanged(INT32 lastgametype)
{
const char *oldgt = NULL, *newgt = NULL;
if (lastgametype >= 0 && lastgametype < gametypecount)
oldgt = Gametype_Names[lastgametype];
if (gametype >= 0 && lastgametype < gametypecount)
newgt = Gametype_Names[gametype];
if (lastgametype >= 0 && lastgametype < numgametypes)
oldgt = gametypes[lastgametype]->name;
if (gametype >= 0 && gametype < numgametypes)
newgt = gametypes[gametype]->name;
if (oldgt && newgt)
CONS_Printf(M_GetText("Gametype was changed from %s to %s\n"), oldgt, newgt);
@ -5014,11 +5035,11 @@ void D_GameTypeChanged(INT32 lastgametype)
{
if (!cv_timelimit.changed) // user hasn't changed limits
{
CV_SetValue(&cv_timelimit, timelimits[gametype]);
CV_SetValue(&cv_timelimit, gametypes[gametype]->timelimit);
}
if (!cv_pointlimit.changed)
{
CV_SetValue(&cv_pointlimit, pointlimits[gametype]);
CV_SetValue(&cv_pointlimit, gametypes[gametype]->pointlimit);
}
}
@ -5313,11 +5334,29 @@ static void Got_SetupVotecmd(UINT8 **cp, INT32 playernum)
// Strip illegal Encore flag.
if ((gt & VOTEMODIFIER_ENCORE)
&& !(gametypedefaultrules[(gt & ~VOTEMODIFIER_ENCORE)] & GTR_CIRCUIT))
&& !(gametypes[(gt & ~VOTEMODIFIER_ENCORE)]->rules & GTR_ENCORE))
{
gt &= ~VOTEMODIFIER_ENCORE;
}
if ((gt & ~VOTEMODIFIER_ENCORE) >= numgametypes)
{
gt &= ~VOTEMODIFIER_ENCORE;
if (server)
I_Error("Got_SetupVotecmd: Internal gametype ID %d not found (numgametypes = %d)", gt, numgametypes);
CONS_Alert(CONS_WARNING, M_GetText("Vote setup with bad gametype ID %d received from %s\n"), gt, player_names[playernum]);
return;
}
if ((secondgt & ~VOTEMODIFIER_ENCORE) >= numgametypes)
{
secondgt &= ~VOTEMODIFIER_ENCORE;
if (server)
I_Error("Got_SetupVotecmd: Internal second gametype ID %d not found (numgametypes = %d)", secondgt, numgametypes);
CONS_Alert(CONS_WARNING, M_GetText("Vote setup with bad second gametype ID %d received from %s\n"), secondgt, player_names[playernum]);
return;
}
for (i = 0; i < 4; i++)
{
tempvotelevels[i][0] = (UINT16)READUINT16(*cp);
@ -5333,11 +5372,11 @@ static void Got_SetupVotecmd(UINT8 **cp, INT32 playernum)
// If third entry has an illelegal Encore flag... (illelegal!?)
if ((secondgt & VOTEMODIFIER_ENCORE)
&& !(gametypedefaultrules[(secondgt & ~VOTEMODIFIER_ENCORE)] & GTR_CIRCUIT))
&& !(gametypes[(secondgt & ~VOTEMODIFIER_ENCORE)]->rules & GTR_ENCORE))
{
secondgt &= ~VOTEMODIFIER_ENCORE;
// Apply it to the second entry instead, gametype permitting!
if (gametypedefaultrules[gt] & GTR_CIRCUIT)
if (gametypes[gt]->rules & GTR_ENCORE)
{
tempvotelevels[1][1] |= VOTEMODIFIER_ENCORE;
}
@ -5737,11 +5776,11 @@ void Command_Retry_f(void)
{
CONS_Printf(M_GetText("You must be in a level to use this.\n"));
}
else if (grandprixinfo.gp == false && bossinfo.boss == false)
else if (grandprixinfo.gp == false)
{
CONS_Printf(M_GetText("This only works in singleplayer games.\n"));
}
else if (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE)
else if (grandprixinfo.eventmode == GPEVENT_BONUS)
{
CONS_Printf(M_GetText("You can't retry right now!\n"));
}
@ -5778,7 +5817,7 @@ static void Command_Togglemodified_f(void)
static void Command_Archivetest_f(void)
{
savebuffer_t save;
savebuffer_t save = {0};
UINT32 i, wrote;
thinker_t *th;
if (gamestate != GS_LEVEL)
@ -5794,9 +5833,11 @@ static void Command_Archivetest_f(void)
((mobj_t *)th)->mobjnum = i++;
// allocate buffer
save.size = 1024;
save.buffer = save.p = ZZ_Alloc(save.size);
save.end = save.buffer + save.size;
if (P_SaveBufferAlloc(&save, 1024) == false)
{
CONS_Printf("Unable to allocate buffer.\n");
return;
}
// test archive
CONS_Printf("LUA_Archive...\n");
@ -5814,10 +5855,12 @@ static void Command_Archivetest_f(void)
LUA_UnArchive(&save, true);
i = READUINT8(save.p);
if (i != 0x7F || wrote != (UINT32)(save.p - save.buffer))
{
CONS_Printf("Savegame corrupted. (write %u, read %u)\n", wrote, (UINT32)(save.p - save.buffer));
}
// free buffer
Z_Free(save.buffer);
P_SaveBufferFree(&save);
CONS_Printf("Done. No crash.\n");
}
#endif

View file

@ -83,8 +83,6 @@ extern consvar_t cv_kartspeed;
extern consvar_t cv_kartbumpers;
extern consvar_t cv_kartfrantic;
extern consvar_t cv_kartencore;
extern consvar_t cv_kartvoterulechanges;
extern consvar_t cv_kartgametypepreference;
extern consvar_t cv_kartspeedometer;
extern consvar_t cv_kartvoices;
extern consvar_t cv_kartbot;

View file

@ -99,7 +99,6 @@ static filetran_t transfer[MAXNETNODES];
INT32 fileneedednum; // Number of files needed to join the server
fileneeded_t fileneeded[MAX_WADFILES]; // List of needed files
static tic_t lasttimeackpacketsent = 0;
char downloaddir[512] = "DOWNLOAD";
// For resuming failed downloads
typedef struct

View file

@ -62,7 +62,8 @@ struct fileneeded_t
extern INT32 fileneedednum;
extern fileneeded_t fileneeded[MAX_WADFILES];
extern char downloaddir[512];
#define DOWNLOADDIR_PART "downloads"
extern char downloaddir[];
extern INT32 lastfilenum;
extern INT32 downloadcompletednum;

View file

@ -309,11 +309,16 @@ static inline int lib_getenum(lua_State *L)
}
else if (fastncmp("GT_", word, 3)) {
p = word;
for (i = 0; Gametype_ConstantNames[i]; i++)
if (fastcmp(p, Gametype_ConstantNames[i])) {
i = 0;
while (gametypes[i] != NULL)
{
if (fastcmp(p, gametypes[i]->constant))
{
lua_pushinteger(L, i);
return 1;
}
i++;
}
if (mathlib) return luaL_error(L, "gametype '%s' could not be found.\n", word);
return 0;
}

View file

@ -764,13 +764,13 @@ void readgametype(MYFILE *f, char *gtname)
char *tmp;
INT32 i, j;
INT16 newgtidx = 0;
gametype_t *newgametype = NULL;
UINT32 newgtrules = 0;
UINT32 newgttol = 0;
INT32 newgtpointlimit = 0;
INT32 newgttimelimit = 0;
INT16 newgtrankingstype = -1;
int newgtinttype = 0;
UINT8 newgtinttype = 0;
char gtconst[MAXLINELEN];
// Empty strings.
@ -821,12 +821,6 @@ void readgametype(MYFILE *f, char *gtname)
newgtpointlimit = (INT32)i;
else if (fastcmp(word, "DEFAULTTIMELIMIT"))
newgttimelimit = (INT32)i;
// Rankings type
else if (fastcmp(word, "RANKINGTYPE"))
{
// Case insensitive
newgtrankingstype = (int)get_number(word2);
}
// Intermission type
else if (fastcmp(word, "INTERMISSIONTYPE"))
{
@ -879,36 +873,54 @@ void readgametype(MYFILE *f, char *gtname)
Z_Free(word2lwr);
// Ran out of gametype slots
if (gametypecount == NUMGAMETYPEFREESLOTS)
if (numgametypes == GT_LASTFREESLOT)
{
CONS_Alert(CONS_WARNING, "Ran out of free gametype slots!\n");
I_Error("Out of Gametype Freeslots while allocating \"%s\"\nLoad less addons to fix this.", gtname);
}
if (gtname[0] == '\0')
{
deh_warning("Custom gametype must have a name");
return;
}
if (strlen(gtname) >= MAXGAMETYPELENGTH)
{
deh_warning("Custom gametype \"%s\"'s name must be %d long at most", gtname, MAXGAMETYPELENGTH-1);
return;
}
for (i = 0; i < numgametypes; i++)
if (fastcmp(gtname, gametypes[i]->name))
break;
if (i < numgametypes)
{
deh_warning("Custom gametype \"%s\"'s name is already in use", gtname);
return;
}
// Add the new gametype
newgtidx = G_AddGametype(newgtrules);
G_AddGametypeTOL(newgtidx, newgttol);
newgametype = Z_Calloc(sizeof (gametype_t), PU_STATIC, NULL);
if (!newgametype)
{
I_Error("Out of memory allocating gametype \"%s\"", gtname);
}
// Not covered by G_AddGametype alone.
if (newgtrankingstype == -1)
newgtrankingstype = newgtidx;
gametyperankings[newgtidx] = newgtrankingstype;
intermissiontypes[newgtidx] = newgtinttype;
pointlimits[newgtidx] = newgtpointlimit;
timelimits[newgtidx] = newgttimelimit;
// Write the new gametype name.
Gametype_Names[newgtidx] = Z_StrDup((const char *)gtname);
// Write the constant name.
if (gtconst[0] == '\0')
strncpy(gtconst, gtname, MAXLINELEN);
G_AddGametypeConstant(newgtidx, (const char *)gtconst);
// Update gametype_cons_t accordingly.
G_UpdateGametypeSelections();
newgametype->name = Z_StrDup((const char *)gtname);
newgametype->rules = newgtrules;
newgametype->constant = G_PrepareGametypeConstant((const char *)gtconst);
newgametype->tol = newgttol;
newgametype->intermission = newgtinttype;
newgametype->pointlimit = newgtpointlimit;
newgametype->timelimit = newgttimelimit;
CONS_Printf("Added gametype %s\n", Gametype_Names[newgtidx]);
gametypes[numgametypes++] = newgametype;
CONS_Printf("Added gametype %s\n", gtname);
}
void readlevelheader(MYFILE *f, char * name)
@ -1128,7 +1140,7 @@ void readlevelheader(MYFILE *f, char * name)
}
else if (fastcmp(word, "TYPEOFLEVEL"))
{
if (i) // it's just a number
if (i || isdigit(word2[0])) // it's just a number
mapheaderinfo[num]->typeoflevel = (UINT32)i;
else
{
@ -1267,12 +1279,12 @@ void readlevelheader(MYFILE *f, char * name)
else
mapheaderinfo[num]->menuflags &= ~LF2_NOTIMEATTACK;
}
else if (fastcmp(word, "VISITNEEDED"))
else if (fastcmp(word, "FINISHNEEDED"))
{
if (i || word2[0] == 'T' || word2[0] == 'Y')
mapheaderinfo[num]->menuflags |= LF2_VISITNEEDED;
mapheaderinfo[num]->menuflags |= LF2_FINISHNEEDED;
else
mapheaderinfo[num]->menuflags &= ~LF2_VISITNEEDED;
mapheaderinfo[num]->menuflags &= ~LF2_FINISHNEEDED;
}
else if (fastcmp(word, "GRAVITY"))
mapheaderinfo[num]->gravity = FLOAT_TO_FIXED(atof(word2));
@ -2267,6 +2279,8 @@ void readunlockable(MYFILE *f, INT32 num)
unlockables[num].type = SECRET_TIMEATTACK;
else if (fastcmp(word2, "BREAKTHECAPSULES"))
unlockables[num].type = SECRET_BREAKTHECAPSULES;
else if (fastcmp(word2, "SPECIALATTACK"))
unlockables[num].type = SECRET_SPECIALATTACK;
else if (fastcmp(word2, "SOUNDTEST"))
unlockables[num].type = SECRET_SOUNDTEST;
else if (fastcmp(word2, "ALTTITLE"))
@ -3670,7 +3684,7 @@ sfxenum_t get_sfx(const char *word)
return atoi(word);
if (fastncmp("GT_",word,3))
word += 3; // take off the GT_
for (i = 0; i < NUMGAMETYPES; i++)
for (i = 0; i < MAXGAMETYPES; i++)
if (fastcmp(word, Gametype_ConstantNames[i]+3))
return i;
deh_warning("Couldn't find gametype named 'GT_%s'",word);

View file

@ -3283,6 +3283,24 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
//"S_ITEMCAPSULE_BOTTOM",
//"S_ITEMCAPSULE_INSIDE",
"S_MONITOR_DAMAGE",
"S_MONITOR_DEATH",
"S_MONITOR_SCREEN1A",
"S_MONITOR_SCREEN1B",
"S_MONITOR_SCREEN2A",
"S_MONITOR_SCREEN2B",
"S_MONITOR_SCREEN3A",
"S_MONITOR_SCREEN3B",
"S_MONITOR_SCREEN4A",
"S_MONITOR_SCREEN4B",
"S_MONITOR_STAND",
"S_MONITOR_CRACKA",
"S_MONITOR_CRACKB",
"S_MONITOR_BIG_SHARD",
"S_MONITOR_SMALL_SHARD",
"S_MONITOR_TWINKLE",
"S_MAGICIANBOX",
"S_MAGICIANBOXTOP",
"S_MAGICIANBOXBOTTOM",
@ -5298,6 +5316,9 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t
"MT_FLOATINGITEM",
"MT_ITEMCAPSULE",
"MT_ITEMCAPSULE_PART",
"MT_MONITOR",
"MT_MONITOR_PART",
"MT_MONITOR_SHARD",
"MT_MAGICIANBOX",
"MT_SIGNSPARKLE",
@ -5791,38 +5812,36 @@ const char *const PLAYERFLAG_LIST[] = {
};
const char *const GAMETYPERULE_LIST[] = {
"CAMPAIGN",
"RINGSLINGER",
"SPECTATORS",
"LIVES",
"TEAMS",
"FIRSTPERSON",
"CIRCUIT",
"BOTS",
"BUMPERS",
"SPHERES",
"CLOSERPLAYERS",
"BATTLESTARTS",
"PAPERITEMS",
"POWERSTONES",
"TEAMFLAGS",
"FRIENDLY",
"SPECIALSTAGES",
"EMERALDTOKENS",
"EMERALDHUNT",
"RACE",
"TAG",
"KARMA",
"ITEMARROWS",
"CAPSULES",
"CATCHER",
"ROLLINGSTART",
"SPECIALSTART",
"BOSS",
"POINTLIMIT",
"TIMELIMIT",
"OVERTIME",
"HURTMESSAGES",
"FRIENDLYFIRE",
"STARTCOUNTDOWN",
"HIDEFROZEN",
"BLINDFOLDED",
"RESPAWNDELAY",
"PITYSHIELD",
"DEATHPENALTY",
"NOSPECTATORSPAWN",
"DEATHMATCHSTARTS",
"SPAWNINVUL",
"SPAWNENEMIES",
"ALLOWEXIT",
"NOTITLECARD",
"CUTSCENES",
"ENCORE",
"TEAMS",
"NOTEAMS",
"TEAMSTARTS",
"NOMP",
"NOCUPSELECT",
NULL
};
@ -6305,7 +6324,7 @@ struct int_const_s const INT_CONST[] = {
{"LF2_HIDEINMENU",LF2_HIDEINMENU},
{"LF2_HIDEINSTATS",LF2_HIDEINSTATS},
{"LF2_NOTIMEATTACK",LF2_NOTIMEATTACK},
{"LF2_VISITNEEDED",LF2_VISITNEEDED},
{"LF2_FINISHNEEDED",LF2_FINISHNEEDED},
// Emeralds
{"EMERALD_CHAOS1",EMERALD_CHAOS1},
@ -6403,9 +6422,9 @@ struct int_const_s const INT_CONST[] = {
// Intermission types
{"int_none",int_none},
{"int_race",int_race},
{"int_battle",int_battle},
{"int_battletime", int_battletime},
{"int_time",int_time},
{"int_score",int_score},
{"int_scoreortimeattack", int_scoreortimeattack},
// Jingles (jingletype_t)
{"JT_NONE",JT_NONE},

View file

@ -499,8 +499,8 @@ void DRPC_UpdatePresence(void)
else
{
snprintf(detailstr, 48, "%s%s%s",
gametype_cons_t[gametype].strvalue,
(gametype == GT_RACE) ? va(" | %s", kartspeed_cons_t[gamespeed].strvalue) : "",
gametypes[gametype]->name,
(gametyperules & GTR_CIRCUIT) ? va(" | %s", kartspeed_cons_t[gamespeed].strvalue) : "",
(encoremode == true) ? " | Encore" : ""
);
discordPresence.details = detailstr;

View file

@ -664,7 +664,8 @@ UINT32 quickncasehash (const char *p, size_t n)
// Compile date and time and revision.
extern const char *compdate, *comptime, *comprevision, *compbranch;
extern int compuncommitted;
extern int compuncommitted, compoptimized;
extern const char *comptype;
// Disabled code and code under testing
// None of these that are disabled in the normal build are guaranteed to work perfectly

View file

@ -135,9 +135,9 @@ extern boolean usedCheats;
extern boolean imcontinuing; // Temporary flag while continuing
extern boolean metalrecording;
#define ATTACKING_NONE 0
#define ATTACKING_TIME 1
#define ATTACKING_CAPSULES 2
#define ATTACKING_NONE 0
#define ATTACKING_TIME 1
#define ATTACKING_LAP (1<<1)
extern UINT8 modeattacking;
// menu demo things
@ -151,15 +151,9 @@ extern boolean addedtogame; // true after the server has added you
// Only true if >1 player. netgame => multiplayer but not (multiplayer=>netgame)
extern boolean multiplayer;
extern INT16 gametype;
extern UINT32 gametyperules;
extern INT16 gametypecount;
extern UINT8 splitscreen;
extern int r_splitscreen;
extern boolean circuitmap; // Does this level have 'circuit mode'?
extern boolean fromlevelselect;
extern boolean forceresetplayers, deferencoremode;
@ -449,71 +443,97 @@ struct mapheader_t
#define LF_SECTIONRACE (1<<2) ///< Section race level
#define LF_SUBTRACTNUM (1<<3) ///< Use subtractive position number (for bright levels)
#define LF2_HIDEINMENU (1<<0) ///< Hide in the multiplayer menu
#define LF2_HIDEINSTATS (1<<1) ///< Hide in the statistics screen
#define LF2_NOTIMEATTACK (1<<2) ///< Hide this map in Time Attack modes
#define LF2_VISITNEEDED (1<<3) ///< Not available in Time Attack modes until you visit the level
#define LF2_HIDEINMENU (1<<0) ///< Hide in the multiplayer menu
#define LF2_HIDEINSTATS (1<<1) ///< Hide in the statistics screen
#define LF2_NOTIMEATTACK (1<<2) ///< Hide this map in Time Attack modes
#define LF2_FINISHNEEDED (1<<3) ///< Not available in Time Attack modes until you beat the level
extern mapheader_t** mapheaderinfo;
extern INT32 nummapheaders, mapallocsize;
// Gametypes
#define NUMGAMETYPEFREESLOTS 128
#define NUMGAMETYPEFREESLOTS (MAXGAMETYPES-GT_FIRSTFREESLOT)
#define MAXGAMETYPELENGTH 32
enum GameType
{
GT_RACE = 0,
GT_BATTLE,
GT_SPECIAL,
GT_VERSUS,
GT_FIRSTFREESLOT,
GT_LASTFREESLOT = GT_FIRSTFREESLOT + NUMGAMETYPEFREESLOTS - 1,
NUMGAMETYPES
GT_LASTFREESLOT = 127, // Previously (GT_FIRSTFREESLOT + NUMGAMETYPEFREESLOTS - 1) - it would be necessary to rewrite VOTEMODIFIER_ENCORE to go higher than this.
MAXGAMETYPES
};
// If you alter this list, update deh_tables.c, MISC_ChangeGameTypeMenu in m_menu.c, and Gametype_Names in g_game.c
// If you alter this list, update defaultgametypes and *gametypes in g_game.c
#define MAXTOL (1<<31)
#define NUMBASETOLNAMES (5)
#define NUMTOLNAMES (NUMBASETOLNAMES + NUMGAMETYPEFREESLOTS)
struct gametype_t
{
const char *name;
const char *constant;
UINT32 rules;
UINT32 tol;
UINT8 intermission;
INT32 pointlimit;
INT32 timelimit;
};
extern gametype_t *gametypes[MAXGAMETYPES+1];
extern INT16 numgametypes;
extern INT16 gametype;
// Gametype rules
enum GameTypeRules
{
// Race rules
GTR_CIRCUIT = 1, // Enables the finish line, laps, and the waypoint system.
GTR_BOTS = 1<<2, // Allows bots in this gametype. Combine with BotTiccmd hooks to make bots support your gametype.
GTR_CIRCUIT = 1, // Enables the finish line, laps, and the waypoint system.
GTR_BOTS = 1<<1, // Allows bots in this gametype. Combine with BotTiccmd hooks to make bots support your gametype.
// Battle gametype rules
GTR_BUMPERS = 1<<3, // Enables the bumper health system
GTR_SPHERES = 1<<4, // Replaces rings with blue spheres
GTR_PAPERITEMS = 1<<5, // Replaces item boxes with paper item spawners
GTR_WANTED = 1<<6, // unused
GTR_KARMA = 1<<7, // Enables the Karma system if you're out of bumpers
GTR_ITEMARROWS = 1<<8, // Show item box arrows above players
GTR_CAPSULES = 1<<9, // Enables the wanted anti-camping system
GTR_BATTLESTARTS = 1<<10, // Use Battle Mode start positions.
GTR_BUMPERS = 1<<2, // Enables the bumper health system
GTR_SPHERES = 1<<3, // Replaces rings with blue spheres
GTR_CLOSERPLAYERS = 1<<4, // Buffs spindash and draft power to bring everyone together, nerfs invincibility and grow to prevent excessive combos
GTR_POINTLIMIT = 1<<11, // Reaching point limit ends the round
GTR_TIMELIMIT = 1<<12, // Reaching time limit ends the round
GTR_OVERTIME = 1<<13, // Allow overtime behavior
GTR_BATTLESTARTS = 1<<5, // Use Battle Mode start positions.
GTR_PAPERITEMS = 1<<6, // Replaces item boxes with paper item spawners
GTR_POWERSTONES = 1<<7, // Battle Emerald collectables.
GTR_KARMA = 1<<8, // Enables the Karma system if you're out of bumpers
GTR_ITEMARROWS = 1<<9, // Show item box arrows above players
// Custom gametype rules
GTR_TEAMS = 1<<14, // Teams are forced on
GTR_NOTEAMS = 1<<15, // Teams are forced off
GTR_TEAMSTARTS = 1<<16, // Use team-based start positions
// Bonus gametype rules
GTR_CAPSULES = 1<<10, // Can enter Break The Capsules mode
GTR_CATCHER = 1<<11, // UFO Catcher (only works with GTR_CIRCUIT)
GTR_ROLLINGSTART = 1<<12, // Rolling start (only works with GTR_CIRCUIT)
GTR_SPECIALSTART = 1<<13, // White fade instant start
GTR_BOSS = 1<<14, // Boss intro and spawning
// Grand Prix rules
GTR_CAMPAIGN = 1<<17, // Handles cup-based progression
GTR_LIVES = 1<<18, // Lives system, players are forced to spectate during Game Over.
GTR_SPECIALBOTS = 1<<19, // Bot difficulty gets stronger between rounds, and the rival system is enabled.
// General purpose rules
GTR_POINTLIMIT = 1<<15, // Reaching point limit ends the round
GTR_TIMELIMIT = 1<<16, // Reaching time limit ends the round
GTR_OVERTIME = 1<<17, // Allow overtime behavior
GTR_ENCORE = 1<<18, // Alternate Encore mirroring, scripting, and texture remapping
GTR_NOCUPSELECT = 1<<20, // Your maps are not selected via cup. ...mutually exclusive with GTR_CAMPAIGN.
GTR_TEAMS = 1<<19, // Teams are forced on
GTR_NOTEAMS = 1<<20, // Teams are forced off
GTR_TEAMSTARTS = 1<<21, // Use team-based start positions
GTR_NOMP = 1<<22, // No multiplayer
GTR_NOCUPSELECT = 1<<23, // Your maps are not selected via cup.
// free: to and including 1<<31
};
// Remember to update GAMETYPERULE_LIST in deh_soc.c
// String names for gametypes
extern const char *Gametype_Names[NUMGAMETYPES];
extern const char *Gametype_ConstantNames[NUMGAMETYPES];
#define GTR_FORBIDMP (GTR_NOMP|GTR_CATCHER|GTR_BOSS)
// Point and time limits for every gametype
extern INT32 pointlimits[NUMGAMETYPES];
extern INT32 timelimits[NUMGAMETYPES];
// TODO: replace every instance
#define gametyperules (gametypes[gametype]->rules)
// TypeOfLevel things
enum TypeOfLevel
@ -527,9 +547,10 @@ enum TypeOfLevel
// Modifiers
TOL_TV = 0x0100 ///< Midnight Channel specific: draw TV like overlay on HUD
};
// Make sure to update TYPEOFLEVEL too
#define MAXTOL (1<<31)
#define NUMBASETOLNAMES (4)
#define NUMBASETOLNAMES (5)
#define NUMTOLNAMES (NUMBASETOLNAMES + NUMGAMETYPEFREESLOTS)
struct tolinfo_t

View file

@ -1889,6 +1889,52 @@ void F_StartTitleScreen(void)
F_CacheTitleScreen();
}
void F_VersionDrawer(void)
{
// An adapted thing from old menus - most games have version info on the title screen now...
INT32 texty = vid.height - 10*vid.dupy;
#define addtext(f, str) {\
V_DrawThinString(vid.dupx, texty, V_NOSCALESTART|f, str);\
texty -= 10*vid.dupy;\
}
if (customversionstring[0] != '\0')
{
addtext(V_ALLOWLOWERCASE, customversionstring);
addtext(0, "Mod version:");
}
else
{
// Development -- show revision / branch info
#if defined(TESTERS)
addtext(V_ALLOWLOWERCASE|V_SKYMAP, "Tester client");
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", compdate));
#elif defined(HOSTTESTERS)
addtext(V_ALLOWLOWERCASE|V_REDMAP, "Netgame host for testers");
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", compdate));
#elif defined(DEVELOP)
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, comprevision);
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, compbranch);
#else // Regular build
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", VERSIONSTRING));
#endif
if (compoptimized)
{
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s build", comptype));
}
else
{
addtext(V_ALLOWLOWERCASE|V_ORANGEMAP, va("%s build (no optimizations)", comptype));
}
if (compuncommitted)
{
addtext(V_REDMAP|V_STRINGDANCE, "! UNCOMMITTED CHANGES !");
}
}
#undef addtext
}
// (no longer) De-Demo'd Title Screen
void F_TitleScreenDrawer(void)
{
@ -1959,39 +2005,7 @@ void F_TitleScreenDrawer(void)
V_DrawFixedPatch(0, 0, FRACUNIT, 0, kts_copyright, NULL);
// An adapted thing from old menus - most games have version info on the title screen now...
{
INT32 texty = vid.height - 10*vid.dupy;
#define addtext(f, str) {\
V_DrawThinString(vid.dupx, texty, V_NOSCALESTART|f, str);\
texty -= 10*vid.dupy;\
}
if (customversionstring[0] != '\0')
{
addtext(V_ALLOWLOWERCASE, customversionstring);
addtext(0, "Mod version:");
}
else
{
// Development -- show revision / branch info
#if defined(TESTERS)
addtext(V_ALLOWLOWERCASE|V_SKYMAP, "Tester client");
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", compdate));
#elif defined(HOSTTESTERS)
addtext(V_ALLOWLOWERCASE|V_REDMAP, "Netgame host for testers");
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", compdate));
#elif defined(DEVELOP)
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, comprevision);
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, compbranch);
#else // Regular build
addtext(V_ALLOWLOWERCASE|V_TRANSLUCENT, va("%s", VERSIONSTRING));
#endif
if (compuncommitted)
addtext(V_REDMAP|V_STRINGDANCE, "! UNCOMMITTED CHANGES !");
}
#undef addtext
}
F_VersionDrawer();
break;
}

View file

@ -60,6 +60,8 @@ void F_EndingDrawer(void);
void F_CreditTicker(void);
void F_CreditDrawer(void);
void F_VersionDrawer(void);
void F_StartCustomCutscene(INT32 cutscenenum, boolean precutscene, boolean resetplayer);
void F_CutsceneDrawer(void);
void F_EndCutScene(void);

View file

@ -310,13 +310,13 @@ closedir (DIR * dirp)
}
#endif
static CV_PossibleValue_t addons_cons_t[] = {{0, "Default"},
static CV_PossibleValue_t addons_cons_t[] = {{0, "Addons"},
#if 1
{1, "HOME"}, {2, "RINGRACERS"},
{1, "HOME"}, {2, "IWAD"},
#endif
{3, "CUSTOM"}, {0, NULL}};
consvar_t cv_addons_option = CVAR_INIT ("addons_option", "Default", CV_SAVE|CV_CALL, addons_cons_t, Addons_option_Onchange);
consvar_t cv_addons_option = CVAR_INIT ("addons_option", "Addons", CV_SAVE|CV_CALL, addons_cons_t, Addons_option_Onchange);
consvar_t cv_addons_folder = CVAR_INIT ("addons_folder", "", CV_SAVE, NULL, NULL);
static CV_PossibleValue_t addons_md5_cons_t[] = {{0, "Name"}, {1, "Contents"}, {0, NULL}};

View file

@ -70,7 +70,7 @@ boolean noblit; // for comparative timing purposes
tic_t demostarttime; // for comparative timing purposes
static char demoname[MAX_WADPATH];
static savebuffer_t demobuf;
static savebuffer_t demobuf = {0};
static UINT8 *demotime_p, *demoinfo_p;
static UINT8 demoflags;
boolean demosynced = true; // console warning message
@ -119,18 +119,14 @@ demoghost *ghosts = NULL;
#define DEMOVERSION 0x0007
#define DEMOHEADER "\xF0" "KartReplay" "\x0F"
#define DF_GHOST 0x01 // This demo contains ghost data too!
#define DF_TIMEATTACK 0x02 // This demo is from Time Attack and contains its final completion time & best lap!
#define DF_BREAKTHECAPSULES 0x04 // This demo is from Break the Capsules and contains its final completion time!
#define DF_ATTACKMASK 0x06 // This demo is from ??? attack and contains ???
#define DF_ATTACKMASK (ATTACKING_TIME|ATTACKING_LAP) // This demo contains time/lap data
// 0x08 free
#define DF_GHOST 0x08 // This demo contains ghost data too!
#define DF_NONETMP 0x10 // multiplayer but not netgame
#define DF_LUAVARS 0x20 // this demo contains extra lua vars
#define DF_ATTACKSHIFT 1
#define DF_ENCORE 0x40
#define DF_MULTIPLAYER 0x80 // This demo was recorded in multiplayer mode!
@ -2017,12 +2013,10 @@ void G_RecordDemo(const char *name)
maxsize = atoi(M_GetNextParm()) * 1024;
// if (demobuf.buffer)
// P_SaveBufferFree(&demobuf);
// Z_Free(demobuf.buffer);
demobuf.size = maxsize;
demobuf.buffer = (UINT8 *)malloc(maxsize);
P_SaveBufferAlloc(&demobuf, maxsize);
demobuf.p = NULL;
demobuf.end = demobuf.buffer + demobuf.size;
demo.recording = true;
}
@ -2034,10 +2028,8 @@ void G_RecordMetal(void)
if (M_CheckParm("-maxdemo") && M_IsNextParm())
maxsize = atoi(M_GetNextParm()) * 1024;
demobuf.size = maxsize;
demobuf.buffer = (UINT8 *)malloc(maxsize);
P_SaveBufferAlloc(&demobuf, maxsize);
demobuf.p = NULL;
demobuf.end = demobuf.buffer + demobuf.size;
metalrecording = true;
}
@ -2345,10 +2337,19 @@ void G_BeginRecording(void)
memset(name,0,sizeof(name));
demobuf.p = demobuf.buffer;
demoflags = DF_GHOST|(multiplayer ? DF_MULTIPLAYER : (modeattacking<<DF_ATTACKSHIFT));
if (multiplayer && !netgame)
demoflags |= DF_NONETMP;
demoflags = DF_GHOST;
if (multiplayer)
{
demoflags |= DF_MULTIPLAYER;
if (!netgame)
demoflags |= DF_NONETMP;
}
else
{
demoflags |= modeattacking;
}
if (encoremode)
demoflags |= DF_ENCORE;
@ -2379,7 +2380,9 @@ void G_BeginRecording(void)
M_Memcpy(demobuf.p, mapmd5, 16); demobuf.p += 16;
WRITEUINT8(demobuf.p, demoflags);
WRITEUINT8(demobuf.p, gametype & 0xFF);
WRITESTRINGN(demobuf.p, gametypes[gametype]->name, MAXGAMETYPELENGTH);
WRITEUINT8(demobuf.p, numlaps);
// file list
@ -2388,21 +2391,18 @@ void G_BeginRecording(void)
// character list
G_SaveDemoSkins(&demobuf.p);
switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
if ((demoflags & DF_ATTACKMASK))
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
demotime_p = demobuf.p;
demotime_p = demobuf.p;
if (demoflags & ATTACKING_TIME)
WRITEUINT32(demobuf.p,UINT32_MAX); // time
if (demoflags & ATTACKING_LAP)
WRITEUINT32(demobuf.p,UINT32_MAX); // lap
break;
case ATTACKING_CAPSULES: // 2
demotime_p = demobuf.p;
WRITEUINT32(demobuf.p,UINT32_MAX); // time
break;
default: // 3
break;
}
else
{
demotime_p = NULL;
}
for (i = 0; i < PRNUMCLASS; i++)
@ -2606,18 +2606,15 @@ void G_SetDemoTime(UINT32 ptime, UINT32 plap)
{
if (!demo.recording || !demotime_p)
return;
if (demoflags & DF_TIMEATTACK)
if (demoflags & ATTACKING_TIME)
{
WRITEUINT32(demotime_p, ptime);
}
if (demoflags & ATTACKING_LAP)
{
WRITEUINT32(demotime_p, plap);
demotime_p = NULL;
}
else if (demoflags & DF_BREAKTHECAPSULES)
{
WRITEUINT32(demotime_p, ptime);
(void)plap;
demotime_p = NULL;
}
demotime_p = NULL;
}
// Returns bitfield:
@ -2628,7 +2625,8 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
{
UINT8 *buffer,*p;
UINT8 flags;
UINT32 oldtime, newtime, oldlap, newlap;
UINT32 oldtime = UINT32_MAX, newtime = UINT32_MAX;
UINT32 oldlap = UINT32_MAX, newlap = UINT32_MAX;
UINT16 oldversion;
size_t bufsize ATTRUNUSED;
UINT8 c;
@ -2658,23 +2656,22 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
SKIPSTRING(p); // gamemap
p += 16; // map md5
flags = READUINT8(p); // demoflags
p++; // gametype
SKIPSTRING(p); // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p);
G_SkipDemoSkins(&p);
aflags = flags & (DF_TIMEATTACK|DF_BREAKTHECAPSULES);
aflags = flags & DF_ATTACKMASK;
I_Assert(aflags);
if (flags & DF_TIMEATTACK)
uselaps = true; // get around uninitalized error
if (aflags & ATTACKING_LAP)
uselaps = true;
newtime = READUINT32(p);
if (aflags & ATTACKING_TIME)
newtime = READUINT32(p);
if (uselaps)
newlap = READUINT32(p);
else
newlap = UINT32_MAX;
Z_Free(buffer);
@ -2718,7 +2715,7 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
SKIPSTRING(p); // gamemap
p += 16; // mapmd5
flags = READUINT8(p);
p++; // gametype
SKIPSTRING(p); // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p);
if (!(flags & aflags))
@ -2730,11 +2727,10 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
G_SkipDemoSkins(&p);
oldtime = READUINT32(p);
if (flags & ATTACKING_TIME)
oldtime = READUINT32(p);
if (uselaps)
oldlap = READUINT32(p);
else
oldlap = 0;
Z_Free(buffer);
@ -2764,7 +2760,7 @@ void G_LoadDemoInfo(menudemo_t *pdemo)
UINT8 version, subversion, pdemoflags, worknumskins, skinid;
democharlist_t *skinlist = NULL;
UINT16 pdemoversion, count;
char mapname[MAXMAPLUMPNAME];
char mapname[MAXMAPLUMPNAME],gtname[MAXGAMETYPELENGTH];
INT32 i;
if (!FIL_ReadFile(pdemo->filepath, &infobuffer))
@ -2828,7 +2824,9 @@ void G_LoadDemoInfo(menudemo_t *pdemo)
return;
}
pdemo->gametype = READUINT8(info_p);
READSTRINGN(info_p, gtname, sizeof(gtname)); // gametype
pdemo->gametype = G_GetGametypeByName(gtname);
pdemo->numlaps = READUINT8(info_p);
pdemo->addonstatus = G_CheckDemoExtraFiles(&info_p, true);
@ -2940,9 +2938,11 @@ void G_DeferedPlayDemo(const char *name)
void G_DoPlayDemo(char *defdemoname)
{
UINT8 i, p, numslots = 0;
INT32 i;
UINT8 p, numslots = 0;
lumpnum_t l;
char color[MAXCOLORNAME+1],follower[17],mapname[MAXMAPLUMPNAME],*n,*pdemoname;
char color[MAXCOLORNAME+1],follower[17],mapname[MAXMAPLUMPNAME],gtname[MAXGAMETYPELENGTH];
char *n,*pdemoname;
UINT8 availabilities[MAXPLAYERS][MAXAVAILABILITY];
UINT8 version,subversion;
UINT32 randseed[PRNUMCLASS];
@ -2959,6 +2959,7 @@ void G_DoPlayDemo(char *defdemoname)
follower[16] = '\0';
color[MAXCOLORNAME] = '\0';
gtname[MAXGAMETYPELENGTH-1] = '\0';
// No demo name means we're restarting the current demo
if (defdemoname == NULL)
@ -2982,7 +2983,7 @@ void G_DoPlayDemo(char *defdemoname)
if (FIL_CheckExtension(defdemoname))
{
//FIL_DefaultExtension(defdemoname, ".lmp");
if (!FIL_ReadFile(defdemoname, &demobuf.buffer))
if (P_SaveBufferFromFile(&demobuf, defdemoname) == false)
{
snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
@ -2990,20 +2991,20 @@ void G_DoPlayDemo(char *defdemoname)
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
demobuf.p = demobuf.buffer;
}
// load demo resource from WAD
else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
else
{
snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
gameaction = ga_nothing;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
else // it's an internal demo
{
demobuf.buffer = demobuf.p = W_CacheLumpNum(l, PU_STATIC);
if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
{
snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
gameaction = ga_nothing;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
P_SaveBufferFromLump(&demobuf, l);
#if defined(SKIPERRORS) && !defined(DEVELOP)
skiperrors = true; // SRB2Kart: Don't print warnings for staff ghosts, since they'll inevitably happen when we make bugfixes/changes...
#endif
@ -3068,8 +3069,22 @@ void G_DoPlayDemo(char *defdemoname)
demobuf.p += 16; // mapmd5
demoflags = READUINT8(demobuf.p);
gametype = READUINT8(demobuf.p);
G_SetGametype(gametype);
READSTRINGN(demobuf.p, gtname, sizeof(gtname)); // gametype
i = G_GetGametypeByName(gtname);
if (i < 0)
{
snprintf(msg, 1024, M_GetText("%s is in a gametype that is not currently loaded and cannot be played.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
M_StartMessage(msg, NULL, MM_NOTHING);
Z_Free(pdemoname);
Z_Free(demobuf.buffer);
demo.playback = false;
demo.title = false;
return;
}
G_SetGametype(i);
numlaps = READUINT8(demobuf.p);
if (demo.title) // Titledemos should always play and ought to always be compatible with whatever wadlist is running.
@ -3142,7 +3157,7 @@ void G_DoPlayDemo(char *defdemoname)
return;
}
modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
modeattacking = (demoflags & DF_ATTACKMASK);
multiplayer = !!(demoflags & DF_MULTIPLAYER);
demo.netgame = (multiplayer && !(demoflags & DF_NONETMP));
CON_ToggleOff();
@ -3150,21 +3165,10 @@ void G_DoPlayDemo(char *defdemoname)
hu_demotime = UINT32_MAX;
hu_demolap = UINT32_MAX;
switch (modeattacking)
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
hu_demotime = READUINT32(demobuf.p);
hu_demolap = READUINT32(demobuf.p);
break;
case ATTACKING_CAPSULES: // 2
hu_demotime = READUINT32(demobuf.p);
break;
default: // 3
modeattacking = ATTACKING_NONE;
break;
}
if (modeattacking & ATTACKING_TIME)
hu_demotime = READUINT32(demobuf.p);
if (modeattacking & ATTACKING_LAP)
hu_demolap = READUINT32(demobuf.p);
// Random seed
for (i = 0; i < PRNUMCLASS; i++)
@ -3538,7 +3542,7 @@ void G_AddGhost(char *defdemoname)
return;
}
p++; // gametype
SKIPSTRING(p); // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p); // Don't wanna modify the file list for ghosts.
@ -3551,19 +3555,10 @@ void G_AddGhost(char *defdemoname)
return;
}
switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
p += 8; // demo time, lap
break;
case ATTACKING_CAPSULES: // 2
p += 4; // demo time
break;
default: // 3
break;
}
if (flags & ATTACKING_TIME)
p += 4;
if (flags & ATTACKING_LAP)
p += 4;
for (i = 0; i < PRNUMCLASS; i++)
{
@ -3593,9 +3588,10 @@ void G_AddGhost(char *defdemoname)
p++; // player number - doesn't really need to be checked, TODO maybe support adding multiple players' ghosts at once
// any invalidating flags?
if ((READUINT8(p) & (DEMO_SPECTATOR|DEMO_BOT)) != 0)
i = READUINT8(p);
if ((i & (DEMO_SPECTATOR|DEMO_BOT)) != 0)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot.\n"), pdemoname);
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot (spectator/bot)\n"), pdemoname);
Z_Free(skinlist);
Z_Free(pdemoname);
Z_Free(buffer);
@ -3606,6 +3602,8 @@ void G_AddGhost(char *defdemoname)
M_Memcpy(name, p, 16);
p += 16;
p += MAXAVAILABILITY;
// Skin
i = READUINT8(p);
if (i < worknumskins)
@ -3626,7 +3624,7 @@ void G_AddGhost(char *defdemoname)
if (READUINT8(p) != 0xFF)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot.\n"), pdemoname);
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot (bad terminator)\n"), pdemoname);
Z_Free(skinlist);
Z_Free(pdemoname);
Z_Free(buffer);
@ -3761,25 +3759,16 @@ void G_UpdateStaffGhostName(lumpnum_t l)
goto fail; // we don't NEED to do it here, but whatever
}
p++; // Gametype
SKIPSTRING(p); // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p);
G_SkipDemoSkins(&p);
switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
p += 8; // demo time, lap
break;
case ATTACKING_CAPSULES: // 2
p += 4; // demo time
break;
default: // 3
break;
}
if (flags & ATTACKING_TIME)
p += 4;
if (flags & ATTACKING_LAP)
p += 4;
for (i = 0; i < PRNUMCLASS; i++)
{
@ -3945,7 +3934,7 @@ ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill)
WriteDemoChecksum();
saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuf.buffer, demobuf.p - demobuf.buffer); // finally output the file.
}
free(demobuf.buffer);
Z_Free(demobuf.buffer);
metalrecording = false;
if (saved)
I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
@ -4165,7 +4154,7 @@ void G_SaveDemo(void)
if (FIL_WriteFile(demoname, demobuf.buffer, demobuf.p - demobuf.buffer)) // finally output the file.
demo.savemode = DSM_SAVED;
free(demobuf.buffer);
Z_Free(demobuf.buffer);
demo.recording = false;
if (!modeattacking)

View file

@ -80,13 +80,13 @@ typedef enum {
} menudemotype_e;
struct menudemo_t {
char filepath[256];
char filepath[1023 + 256]; // see M_PrepReplayList and sizeof menupath
menudemotype_e type;
char title[65]; // Null-terminated for string prints
UINT16 map;
UINT8 addonstatus; // What do we need to do addon-wise to play this demo?
UINT8 gametype;
INT16 gametype;
SINT8 kartspeed; // Add OR DF_ENCORE for encore mode, idk
UINT8 numlaps;

View file

@ -61,7 +61,6 @@
#include "k_specialstage.h"
#include "k_bot.h"
#include "doomstat.h"
#include "acs/interface.h"
#ifdef HAVE_DISCORDRPC
#include "discord.h"
@ -318,9 +317,8 @@ typedef struct
{
INT16 *mapbuffer; // Pointer to zone memory
INT32 lastnummapheaders; // Reset if nummapheaders != this
UINT8 counttogametype; // Time to gametype change event
} randmaps_t;
static randmaps_t randmaps = {NULL, 0, 0};
static randmaps_t randmaps = {NULL, 0};
static void G_ResetRandMapBuffer(void)
{
@ -330,7 +328,6 @@ static void G_ResetRandMapBuffer(void)
randmaps.mapbuffer = Z_Malloc(randmaps.lastnummapheaders * sizeof(INT16), PU_STATIC, NULL);
for (i = 0; i < randmaps.lastnummapheaders; i++)
randmaps.mapbuffer[i] = -1;
//intentionally not resetting randmaps.counttogametype here
}
typedef struct joystickvector2_s
@ -518,13 +515,18 @@ static void G_UpdateRecordReplays(void)
players[consoleplayer].realtime = UINT32_MAX;
}
if (((mapheaderinfo[gamemap-1]->mainrecord->time == 0) || (players[consoleplayer].realtime < mapheaderinfo[gamemap-1]->mainrecord->time))
&& (players[consoleplayer].realtime < UINT32_MAX)) // DNF
if (modeattacking & ATTACKING_TIME)
{
mapheaderinfo[gamemap-1]->mainrecord->time = players[consoleplayer].realtime;
if (((mapheaderinfo[gamemap-1]->mainrecord->time == 0) || (players[consoleplayer].realtime < mapheaderinfo[gamemap-1]->mainrecord->time))
&& (players[consoleplayer].realtime < UINT32_MAX)) // DNF
mapheaderinfo[gamemap-1]->mainrecord->time = players[consoleplayer].realtime;
}
else
{
mapheaderinfo[gamemap-1]->mainrecord->time = 0;
}
if (modeattacking == ATTACKING_TIME)
if (modeattacking & ATTACKING_LAP)
{
if ((mapheaderinfo[gamemap-1]->mainrecord->lap == 0) || (bestlap < mapheaderinfo[gamemap-1]->mainrecord->lap))
mapheaderinfo[gamemap-1]->mainrecord->lap = bestlap;
@ -547,27 +549,32 @@ static void G_UpdateRecordReplays(void)
strcat(gpath, PATHSEP);
strcat(gpath, G_BuildMapName(gamemap));
snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, cv_chooseskin.string);
snprintf(lastdemo, 255, "%s-%s-last.lmp", gpath, cv_skin[0].string);
gpath = Z_StrDup(gpath);
if (FIL_FileExists(lastdemo))
if (modeattacking != ATTACKING_NONE && FIL_FileExists(lastdemo))
{
UINT8 *buf;
size_t len = FIL_ReadFile(lastdemo, &buf);
size_t len;
snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, cv_chooseskin.string);
if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
{ // Better time, save this demo.
if (FIL_FileExists(bestdemo))
remove(bestdemo);
FIL_WriteFile(bestdemo, buf, len);
CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
gpath = Z_StrDup(gpath);
len = FIL_ReadFile(lastdemo, &buf);
if (modeattacking & ATTACKING_TIME)
{
snprintf(bestdemo, 255, "%s-%s-time-best.lmp", gpath, cv_skin[0].string);
if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & 1)
{ // Better time, save this demo.
if (FIL_FileExists(bestdemo))
remove(bestdemo);
FIL_WriteFile(bestdemo, buf, len);
CONS_Printf("\x83%s\x80 %s '%s'\n", M_GetText("NEW RECORD TIME!"), M_GetText("Saved replay as"), bestdemo);
}
}
if (modeattacking == ATTACKING_TIME)
if (modeattacking & ATTACKING_LAP)
{
snprintf(bestdemo, 255, "%s-%s-lap-best.lmp", gpath, cv_chooseskin.string);
snprintf(bestdemo, 255, "%s-%s-lap-best.lmp", gpath, cv_skin[0].string);
if (!FIL_FileExists(bestdemo) || G_CmpDemoTime(bestdemo, lastdemo) & (1<<1))
{ // Better lap time, save this demo.
if (FIL_FileExists(bestdemo))
@ -580,9 +587,9 @@ static void G_UpdateRecordReplays(void)
//CONS_Printf("%s '%s'\n", M_GetText("Saved replay as"), lastdemo);
Z_Free(buf);
}
Z_Free(gpath);
Z_Free(gpath);
}
// Check emblems when level data is updated
if ((earnedEmblems = M_CheckLevelEmblems()))
@ -1410,7 +1417,7 @@ void G_StartTitleCard(void)
{
// The title card has been disabled for this map.
// Oh well.
if (!G_IsTitleCardAvailable() || demo.rewinding)
if (demo.rewinding || !G_IsTitleCardAvailable())
{
WipeStageTitle = false;
return;
@ -1425,9 +1432,9 @@ void G_StartTitleCard(void)
// play the sound
{
sfxenum_t kstart = sfx_kstart;
if (bossinfo.boss)
if (K_CheckBossIntro() == true)
kstart = sfx_ssa021;
else if (encoremode)
else if (encoremode == true)
kstart = sfx_ruby2;
S_StartSound(NULL, kstart);
}
@ -1474,11 +1481,17 @@ void G_PreLevelTitleCard(void)
//
boolean G_IsTitleCardAvailable(void)
{
#if 0
// Overwrites all other title card exceptions.
if (K_CheckBossIntro() == true)
return true;
// The current level has no name.
if (!mapheaderinfo[gamemap-1]->lvlttl[0])
return false;
#endif
// Instant white fade.
if (gametyperules & GTR_SPECIALSTART)
return false;
// The title card is available.
return true;
@ -2790,22 +2803,9 @@ mapthing_t *G_FindMapStart(INT32 playernum)
if (!playeringame[playernum])
return NULL;
// -- Spectators --
// Order in platform gametypes: Race->DM->CTF
// And, with deathmatch starts: DM->CTF->Race
if (players[playernum].spectator)
{
// In platform gametypes, spawn in Co-op starts first
// Overriden by GTR_BATTLESTARTS.
if (gametyperules & GTR_BATTLESTARTS && bossinfo.boss == false)
spawnpoint = G_FindBattleStartOrFallback(playernum);
else
spawnpoint = G_FindRaceStartOrFallback(playernum);
}
// -- Grand Prix / Time Attack --
// -- Time Attack --
// Order: Race->DM->CTF
else if (grandprixinfo.gp || modeattacking)
if (K_TimeAttackRules() == true)
spawnpoint = G_FindRaceStartOrFallback(playernum);
// -- CTF --
@ -2896,8 +2896,6 @@ void G_DoReborn(INT32 playernum)
if (oldmo)
G_ChangePlayerReferences(oldmo, players[playernum].mo);
}
ACS_RunPlayerEnterScript(player);
}
void G_AddPlayer(INT32 playernum)
@ -2912,17 +2910,20 @@ void G_ExitLevel(void)
if (gamestate == GS_LEVEL)
{
UINT8 i;
boolean youlost = false;
if (bossinfo.boss == true)
boolean doretry = false;
if (!G_GametypeUsesLives())
; // never force a retry
else if (specialstageinfo.valid == true || (gametyperules & GTR_BOSS))
{
youlost = true;
doretry = true;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && !players[i].spectator && !players[i].bot)
{
if (players[i].bumpers > 0)
if (!K_IsPlayerLosing(&players[i]))
{
youlost = false;
doretry = false;
break;
}
}
@ -2930,10 +2931,10 @@ void G_ExitLevel(void)
}
else if (grandprixinfo.gp == true && grandprixinfo.eventmode == GPEVENT_NONE)
{
youlost = (grandprixinfo.wonround != true);
doretry = (grandprixinfo.wonround != true);
}
if (youlost)
if (doretry)
{
// You didn't win...
@ -3000,28 +3001,98 @@ void G_ExitLevel(void)
}
}
// See also the enum GameType in doomstat.h
const char *Gametype_Names[NUMGAMETYPES] =
static gametype_t defaultgametypes[] =
{
"Race", // GT_RACE
"Battle" // GT_BATTLE
// GT_RACE
{
"Race",
"GT_RACE",
GTR_CIRCUIT|GTR_BOTS|GTR_ENCORE,
TOL_RACE,
int_time,
0,
0,
},
// GT_BATTLE
{
"Battle",
"GT_BATTLE",
GTR_SPHERES|GTR_BUMPERS|GTR_PAPERITEMS|GTR_POWERSTONES|GTR_KARMA|GTR_ITEMARROWS|GTR_CAPSULES|GTR_BATTLESTARTS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME|GTR_CLOSERPLAYERS,
TOL_BATTLE,
int_scoreortimeattack,
0,
2,
},
// GT_SPECIAL
{
"Special",
"GT_SPECIAL",
GTR_CATCHER|GTR_SPECIALSTART|GTR_ROLLINGSTART|GTR_CIRCUIT,
TOL_SPECIAL,
int_time,
0,
0,
},
// GT_VERSUS
{
"Versus",
"GT_VERSUS",
GTR_BOSS|GTR_SPHERES|GTR_BUMPERS|GTR_POINTLIMIT|GTR_CLOSERPLAYERS|GTR_NOCUPSELECT|GTR_ENCORE,
TOL_BOSS,
int_scoreortimeattack,
0,
0,
},
};
// For dehacked
const char *Gametype_ConstantNames[NUMGAMETYPES] =
gametype_t *gametypes[MAXGAMETYPES+1] =
{
"GT_RACE", // GT_RACE
"GT_BATTLE" // GT_BATTLE
&defaultgametypes[GT_RACE],
&defaultgametypes[GT_BATTLE],
&defaultgametypes[GT_SPECIAL],
&defaultgametypes[GT_VERSUS],
};
// Gametype rules
UINT32 gametypedefaultrules[NUMGAMETYPES] =
//
// G_GetGametypeByName
//
// Returns the number for the given gametype name string, or -1 if not valid.
//
INT32 G_GetGametypeByName(const char *gametypestr)
{
// Race
GTR_CAMPAIGN|GTR_CIRCUIT|GTR_BOTS,
// Battle
GTR_SPHERES|GTR_BUMPERS|GTR_PAPERITEMS|GTR_KARMA|GTR_ITEMARROWS|GTR_CAPSULES|GTR_BATTLESTARTS|GTR_POINTLIMIT|GTR_TIMELIMIT|GTR_OVERTIME
};
INT32 i = 0;
while (gametypes[i] != NULL)
{
if (!stricmp(gametypestr, gametypes[i]->name))
return i;
i++;
}
return -1; // unknown gametype
}
//
// G_GuessGametypeByTOL
//
// Returns the first valid number for the given typeoflevel, or -1 if not valid.
//
INT32 G_GuessGametypeByTOL(UINT32 tol)
{
INT32 i = 0;
while (gametypes[i] != NULL)
{
if (tol & gametypes[i]->tol)
return i;
i++;
}
return -1; // unknown gametype
}
//
// G_SetGametype
@ -3030,41 +3101,26 @@ UINT32 gametypedefaultrules[NUMGAMETYPES] =
//
void G_SetGametype(INT16 gtype)
{
if (gtype < 0 || gtype > numgametypes)
{
I_Error("G_SetGametype: Bad gametype change %d (was %d/\"%s\")", gtype, gametype, gametypes[gametype]->name);
}
gametype = gtype;
gametyperules = gametypedefaultrules[gametype];
}
//
// G_AddGametype
//
// Add a gametype. Returns the new gametype number.
//
INT16 G_AddGametype(UINT32 rules)
{
INT16 newgtype = gametypecount;
gametypecount++;
// Set gametype rules.
gametypedefaultrules[newgtype] = rules;
Gametype_Names[newgtype] = "???";
// Update gametype_cons_t accordingly.
G_UpdateGametypeSelections();
return newgtype;
}
//
// G_AddGametypeConstant
// G_PrepareGametypeConstant
//
// Self-explanatory. Filters out "bad" characters.
//
void G_AddGametypeConstant(INT16 gtype, const char *newgtconst)
char *G_PrepareGametypeConstant(const char *newgtconst)
{
size_t r = 0; // read
size_t w = 0; // write
char *gtconst = Z_Calloc(strlen(newgtconst) + 4, PU_STATIC, NULL);
char *tmpconst = Z_Calloc(strlen(newgtconst) + 1, PU_STATIC, NULL);
size_t len = strlen(newgtconst);
char *gtconst = Z_Calloc(len + 4, PU_STATIC, NULL);
char *tmpconst = Z_Calloc(len + 1, PU_STATIC, NULL);
// Copy the gametype name.
strcpy(tmpconst, newgtconst);
@ -3124,42 +3180,10 @@ void G_AddGametypeConstant(INT16 gtype, const char *newgtconst)
// Free the temporary string.
Z_Free(tmpconst);
// Finally, set the constant string.
Gametype_ConstantNames[gtype] = gtconst;
// Finally, return the constant string.
return gtconst;
}
//
// G_UpdateGametypeSelections
//
// Updates gametype_cons_t.
//
void G_UpdateGametypeSelections(void)
{
INT32 i;
for (i = 0; i < gametypecount; i++)
{
gametype_cons_t[i].value = i;
gametype_cons_t[i].strvalue = Gametype_Names[i];
}
gametype_cons_t[NUMGAMETYPES].value = 0;
gametype_cons_t[NUMGAMETYPES].strvalue = NULL;
}
// Gametype rankings
INT16 gametyperankings[NUMGAMETYPES] =
{
GT_RACE,
GT_BATTLE,
};
// Gametype to TOL (Type Of Level)
UINT32 gametypetol[NUMGAMETYPES] =
{
TOL_RACE, // Race
TOL_BATTLE, // Battle
TOL_TV, // Midnight Channel effect
};
tolinfo_t TYPEOFLEVEL[NUMTOLNAMES] = {
{"RACE",TOL_RACE},
{"BATTLE",TOL_BATTLE},
@ -3186,51 +3210,6 @@ void G_AddTOL(UINT32 newtol, const char *tolname)
TYPEOFLEVEL[i].flag = newtol;
}
//
// G_AddGametypeTOL
//
// Assigns a type of level to a gametype.
//
void G_AddGametypeTOL(INT16 gtype, UINT32 newtol)
{
gametypetol[gtype] = newtol;
}
//
// G_GetGametypeByName
//
// Returns the number for the given gametype name string, or -1 if not valid.
//
INT32 G_GetGametypeByName(const char *gametypestr)
{
INT32 i;
for (i = 0; i < gametypecount; i++)
if (!stricmp(gametypestr, Gametype_Names[i]))
return i;
return -1; // unknown gametype
}
//
// G_IsSpecialStage
//
// Returns TRUE if
// the given map is a special stage.
//
boolean G_IsSpecialStage(INT32 mapnum)
{
mapnum--; // gamemap-based to 0 indexed
if (mapnum > nummapheaders || !mapheaderinfo[mapnum])
return false;
if (!mapheaderinfo[mapnum]->cup || mapheaderinfo[mapnum]->cup->cachedlevels[CUPCACHE_SPECIAL] != mapnum)
return false;
return true;
}
//
// G_GametypeUsesLives
//
@ -3243,13 +3222,7 @@ boolean G_GametypeUsesLives(void)
return false;
if ((grandprixinfo.gp == true) // In Grand Prix
&& (gametype == GT_RACE) // NOT in bonus round
&& grandprixinfo.eventmode == GPEVENT_NONE) // NOT in bonus
{
return true;
}
if (bossinfo.boss == true) // Fighting a boss?
&& grandprixinfo.eventmode != GPEVENT_BONUS) // NOT in bonus round
{
return true;
}
@ -3294,100 +3267,30 @@ boolean G_GametypeHasSpectators(void)
//
// G_SometimesGetDifferentGametype
//
// Oh, yeah, and we sometimes flip encore mode on here too.
// Because gametypes are no longer on the vote screen, all this does is sometimes flip encore mode.
// However, it remains a seperate function for long-term possibility.
//
INT16 G_SometimesGetDifferentGametype(UINT8 prefgametype)
INT16 G_SometimesGetDifferentGametype(void)
{
// Most of the gametype references in this condition are intentionally not prefgametype.
// This is so a server CAN continue playing a gametype if they like the taste of it.
// The encore check needs prefgametype so can't use G_RaceGametype...
boolean encorepossible = ((M_SecretUnlocked(SECRET_ENCORE, false) || encorescramble == 1)
&& ((gametyperules|gametypedefaultrules[prefgametype]) & GTR_CIRCUIT));
&& (gametyperules & GTR_ENCORE));
UINT8 encoremodifier = 0;
// -- the below is only necessary if you want to use randmaps.mapbuffer here
//if (randmaps.lastnummapheaders != nummapheaders)
//G_ResetRandMapBuffer();
if (encorepossible)
// FORCE to what was scrambled on intermission?
if (encorepossible && encorescramble != -1)
{
if (encorescramble != -1)
// FORCE to what was scrambled on intermission
if ((encorescramble != 0) != (cv_kartencore.value == 1))
{
encorepossible = (boolean)encorescramble; // FORCE to what was scrambled on intermission
}
else
{
switch (cv_kartvoterulechanges.value)
{
case 3: // always
encorepossible = true;
break;
case 2: // frequent
encorepossible = M_RandomChance(FRACUNIT>>1);
break;
case 1: // sometimes
encorepossible = M_RandomChance(FRACUNIT>>2);
break;
default:
break;
}
}
if (encorepossible != (cv_kartencore.value == 1))
encoremodifier = VOTEMODIFIER_ENCORE;
}
}
if (!cv_kartvoterulechanges.value) // never
return (gametype|encoremodifier);
if (randmaps.counttogametype > 0 && (cv_kartvoterulechanges.value != 3))
{
randmaps.counttogametype--;
return (gametype|encoremodifier);
}
switch (cv_kartvoterulechanges.value) // okay, we're having a gametype change! when's the next one, luv?
{
case 1: // sometimes
randmaps.counttogametype = 5; // per "cup"
break;
default:
// fallthrough - happens when clearing buffer, but needs a reasonable countdown if cvar is modified
case 2: // frequent
randmaps.counttogametype = 2; // ...every 1/2th-ish cup?
break;
}
// Only this response is prefgametype-based.
// todo custom gametypes
if (prefgametype == GT_BATTLE)
{
// Intentionally does not use encoremodifier!
if (cv_kartencore.value == 1)
return (GT_RACE|VOTEMODIFIER_ENCORE);
return (GT_RACE);
}
// This might appear wrong HERE, but the game will display the Encore possibility on the second voting choice instead.
return (GT_BATTLE|encoremodifier);
}
//
// G_GetGametypeColor
//
// Pretty and consistent ^u^
// See also M_GetGametypeColor (if that still exists).
//
UINT8 G_GetGametypeColor(INT16 gt)
{
if (modeattacking) // == ATTACKING_RECORD
return orangemap[0];
if (gt == GT_BATTLE)
return redmap[0];
if (gt == GT_RACE)
return skymap[0];
return 255; // FALLBACK
return (gametype|encoremodifier);
}
/** Get the typeoflevel flag needed to indicate support of a gametype.
@ -3397,7 +3300,9 @@ UINT8 G_GetGametypeColor(INT16 gt)
*/
UINT32 G_TOLFlag(INT32 pgametype)
{
return gametypetol[pgametype];
if (pgametype >= 0 && pgametype < numgametypes)
return gametypes[pgametype]->tol;
return 0;
}
INT16 G_GetFirstMapOfGametype(UINT8 pgametype)
@ -3408,7 +3313,7 @@ INT16 G_GetFirstMapOfGametype(UINT8 pgametype)
templevelsearch.cup = NULL;
templevelsearch.typeoflevel = G_TOLFlag(pgametype);
templevelsearch.cupmode = (!(gametypedefaultrules[pgametype] & GTR_NOCUPSELECT));
templevelsearch.cupmode = (!(gametypes[pgametype]->rules & GTR_NOCUPSELECT));
templevelsearch.timeattack = false;
templevelsearch.checklocked = true;
@ -3500,7 +3405,7 @@ tryagain:
if (!mapheaderinfo[ix] || mapheaderinfo[ix]->lumpnum == LUMPERROR)
continue;
if ((mapheaderinfo[ix]->typeoflevel & tolflags) != tolflags
if (!(mapheaderinfo[ix]->typeoflevel & tolflags)
|| ix == pprevmap
|| M_MapLocked(ix+1)
|| (usehellmaps != (mapheaderinfo[ix]->menuflags & LF2_HIDEINMENU))) // this is bad
@ -3719,7 +3624,6 @@ static void G_HandleSaveLevel(void)
static void G_GetNextMap(void)
{
boolean spec = G_IsSpecialStage(prevmap+1);
INT32 i;
// go to next level
@ -3736,7 +3640,7 @@ static void G_GetNextMap(void)
}
else
{
INT32 lastgametype = gametype;
INT32 lastgametype = gametype, newgametype = GT_RACE;
// 5 levels, 2 bonus stages: after rounds 2 and 4 (but flexible enough to accomodate other solutions)
UINT8 bonusmodulo = (grandprixinfo.cup->numlevels+1)/(grandprixinfo.cup->numbonus+1);
UINT8 bonusindex = (grandprixinfo.roundnum / bonusmodulo) - 1;
@ -3753,9 +3657,6 @@ static void G_GetNextMap(void)
G_SetGametype(GT_RACE);
if (gametype != lastgametype)
D_GameTypeChanged(lastgametype);
specialStage.active = false;
bossinfo.boss = false;
}
// Special stage
else if (grandprixinfo.roundnum >= grandprixinfo.cup->numlevels)
@ -3776,11 +3677,11 @@ static void G_GetNextMap(void)
if (totaltotalring >= 50)
{
const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[CUPCACHE_SPECIAL];
if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum]
&& mapheaderinfo[cupLevelNum]->typeoflevel & (TOL_SPECIAL|TOL_BOSS|TOL_BATTLE))
if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum])
{
grandprixinfo.eventmode = GPEVENT_SPECIAL;
nextmap = cupLevelNum;
newgametype = G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel);
}
}
}
@ -3791,37 +3692,27 @@ static void G_GetNextMap(void)
// todo any other condition?
{
const INT32 cupLevelNum = grandprixinfo.cup->cachedlevels[CUPCACHE_BONUS + bonusindex];
if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum]
&& mapheaderinfo[cupLevelNum]->typeoflevel & (TOL_BOSS|TOL_BATTLE))
if (cupLevelNum < nummapheaders && mapheaderinfo[cupLevelNum])
{
grandprixinfo.eventmode = GPEVENT_BONUS;
nextmap = cupLevelNum;
newgametype = G_GuessGametypeByTOL(mapheaderinfo[cupLevelNum]->typeoflevel);
}
}
}
if (newgametype == -1)
{
// Don't permit invalid changes.
grandprixinfo.eventmode = GPEVENT_NONE;
newgametype = gametype;
}
if (grandprixinfo.eventmode != GPEVENT_NONE)
{
// nextmap is set above
const INT32 newtol = mapheaderinfo[nextmap]->typeoflevel;
if (newtol & TOL_SPECIAL)
{
specialStage.active = true;
specialStage.encore = grandprixinfo.encore;
}
else //(if newtol & (TOL_BATTLE|TOL_BOSS)) -- safe to assume??
{
G_SetGametype(GT_BATTLE);
if (gametype != lastgametype)
D_GameTypeChanged(lastgametype);
if (newtol & TOL_BOSS)
{
K_ResetBossInfo();
bossinfo.boss = true;
bossinfo.encore = grandprixinfo.encore;
}
}
G_SetGametype(newgametype);
if (gametype != lastgametype)
D_GameTypeChanged(lastgametype);
}
else if (grandprixinfo.roundnum >= grandprixinfo.cup->numlevels) // On final map
{
@ -3845,10 +3736,6 @@ static void G_GetNextMap(void)
}
}
}
else if (bossinfo.boss == true)
{
nextmap = NEXTMAP_TITLE; // temporary
}
else
{
UINT32 tolflag = G_TOLFlag(gametype);
@ -3987,7 +3874,9 @@ static void G_GetNextMap(void)
if (nextmap == NEXTMAP_INVALID || (nextmap < NEXTMAP_SPECIAL && (nextmap >= nummapheaders || !mapheaderinfo[nextmap] || mapheaderinfo[nextmap]->lumpnum == LUMPERROR)))
I_Error("G_GetNextMap: Internal map ID %d not found (nummapheaders = %d)\n", nextmap, nummapheaders);
#if 0 // This is a surprise tool that will help us later.
if (!spec)
#endif //#if 0
lastmap = nextmap;
}
@ -4111,7 +4000,7 @@ void G_AfterIntermission(void)
G_HandleSaveLevel();
}
if ((gametyperules & GTR_CAMPAIGN) && mapheaderinfo[prevmap]->cutscenenum && !modeattacking && skipstats <= 1 && (gamecomplete || !(marathonmode & MA_NOCUTSCENES))) // Start a custom cutscene.
if ((grandprixinfo.gp == true) && mapheaderinfo[prevmap]->cutscenenum && !modeattacking && skipstats <= 1 && (gamecomplete || !(marathonmode & MA_NOCUTSCENES))) // Start a custom cutscene.
F_StartCustomCutscene(mapheaderinfo[prevmap]->cutscenenum-1, false, false);
else
{
@ -4248,7 +4137,7 @@ void G_EndGame(void)
}
// Only do evaluation and credits in singleplayer contexts
if (!netgame && (gametyperules & GTR_CAMPAIGN))
if (!netgame && grandprixinfo.gp == true)
{
if (nextmap == NEXTMAP_CEREMONY) // end game with ceremony
{
@ -4306,12 +4195,11 @@ void G_LoadGameSettings(void)
// Loads the main data file, which stores information such as emblems found, etc.
void G_LoadGameData(void)
{
size_t length;
UINT32 i, j;
UINT32 versionID;
UINT8 versionMinor;
UINT8 rtemp;
savebuffer_t save;
savebuffer_t save = {0};
//For records
UINT32 numgamedatamapheaders;
@ -4340,16 +4228,13 @@ void G_LoadGameData(void)
return;
}
length = FIL_ReadFile(va(pandf, srb2home, gamedatafilename), &save.buffer);
if (!length)
if (P_SaveBufferFromFile(&save, va(pandf, srb2home, gamedatafilename)) == false)
{
// No gamedata. We can save a new one.
gamedata->loaded = true;
return;
}
save.p = save.buffer;
// Version check
versionID = READUINT32(save.p);
if (versionID != GD_VERSIONCHECK)
@ -4358,16 +4243,14 @@ void G_LoadGameData(void)
if (strcmp(srb2home,"."))
gdfolder = srb2home;
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
I_Error("Game data is not for Ring Racers v2.0.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder);
}
versionMinor = READUINT8(save.p);
if (versionMinor > GD_VERSIONMINOR)
{
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
I_Error("Game data is from the future! (expected %d, got %d)", GD_VERSIONMINOR, versionMinor);
}
@ -4478,8 +4361,7 @@ void G_LoadGameData(void)
}
// done
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
// Don't consider loaded until it's a success!
// It used to do this much earlier, but this would cause the gamedata to
@ -4499,8 +4381,7 @@ void G_LoadGameData(void)
if (strcmp(srb2home,"."))
gdfolder = srb2home;
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
I_Error("Corrupt game data file.\nDelete %s(maybe in %s) and try again.", gamedatafilename, gdfolder);
}
@ -4513,7 +4394,7 @@ void G_SaveGameData(void)
size_t length;
INT32 i, j;
UINT8 btemp;
savebuffer_t save;
savebuffer_t save = {0};
if (!gamedata->loaded)
return; // If never loaded (-nodata), don't save
@ -4533,14 +4414,11 @@ void G_SaveGameData(void)
}
length += nummapheaders * (MAXMAPLUMPNAME+1+4+4);
save.size = length;
save.p = save.buffer = (UINT8 *)malloc(save.size);
if (!save.p)
if (P_SaveBufferAlloc(&save, length) == false)
{
CONS_Alert(CONS_ERROR, M_GetText("No more free memory for saving game data\n"));
return;
}
save.end = save.buffer + save.size;
// Version test
@ -4627,7 +4505,7 @@ void G_SaveGameData(void)
length = save.p - save.buffer;
FIL_WriteFile(va(pandf, srb2home, gamedatafilename), save.buffer, length);
free(save.buffer);
P_SaveBufferFree(&save);
// Also save profiles here.
PR_SaveProfiles();
@ -4641,10 +4519,9 @@ void G_SaveGameData(void)
//
void G_LoadGame(UINT32 slot, INT16 mapoverride)
{
size_t length;
char vcheck[VERSIONSIZE];
char savename[255];
savebuffer_t save;
savebuffer_t save = {0};
// memset savedata to all 0, fixes calling perfectly valid saves corrupt because of bots
memset(&savedata, 0, sizeof(savedata));
@ -4659,17 +4536,12 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
else
sprintf(savename, savegamename, slot);
length = FIL_ReadFile(savename, &save.buffer);
if (!length)
if (P_SaveBufferFromFile(&save, savename) == false)
{
CONS_Printf(M_GetText("Couldn't read file %s\n"), savename);
return;
}
save.p = save.buffer;
save.size = length;
save.end = save.buffer + save.size;
memset(vcheck, 0, sizeof (vcheck));
sprintf(vcheck, (marathonmode ? "back-up %d" : "version %d"), VERSION);
if (strcmp((const char *)save.p, (const char *)vcheck))
@ -4682,7 +4554,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
M_ClearMenus(true); // so ESC backs out to title
M_StartMessage(M_GetText("Save game from different version\n\nPress ESC\n"), NULL, MM_NOTHING);
Command_ExitGame_f();
Z_Free(save.buffer);
P_SaveBufferFree(&save);
// no cheating!
memset(&savedata, 0, sizeof(savedata));
@ -4717,7 +4589,7 @@ void G_LoadGame(UINT32 slot, INT16 mapoverride)
}
// done
Z_Free(save.buffer);
P_SaveBufferFree(&save);
// gameaction = ga_nothing;
// G_SetGamestate(GS_LEVEL);
@ -4743,7 +4615,7 @@ void G_SaveGame(UINT32 slot, INT16 mapnum)
boolean saved;
char savename[256] = "";
const char *backup;
savebuffer_t save;
savebuffer_t save = {0};
if (marathonmode)
strcpy(savename, liveeventbackup);
@ -4756,14 +4628,11 @@ void G_SaveGame(UINT32 slot, INT16 mapnum)
char name[VERSIONSIZE];
size_t length;
save.size = SAVEGAMESIZE;
save.p = save.buffer = (UINT8 *)malloc(save.size);
if (!save.p)
if (P_SaveBufferAlloc(&save, SAVEGAMESIZE) == false)
{
CONS_Alert(CONS_ERROR, M_GetText("No more free memory for saving game data\n"));
return;
}
save.end = save.buffer + save.size;
memset(name, 0, sizeof (name));
sprintf(name, (marathonmode ? "back-up %d" : "version %d"), VERSION);
@ -4781,7 +4650,7 @@ void G_SaveGame(UINT32 slot, INT16 mapnum)
length = save.p - save.buffer;
saved = FIL_WriteFile(backup, save.buffer, length);
free(save.buffer);
P_SaveBufferFree(&save);
}
gameaction = ga_nothing;
@ -4801,7 +4670,7 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
char vcheck[VERSIONSIZE];
char savename[255];
const char *backup;
savebuffer_t save;
savebuffer_t save = {0};
if (marathonmode)
strcpy(savename, liveeventbackup);
@ -4809,22 +4678,19 @@ void G_SaveGameOver(UINT32 slot, boolean modifylives)
sprintf(savename, savegamename, slot);
backup = va("%s",savename);
length = FIL_ReadFile(savename, &save.buffer);
if (!length)
if (P_SaveBufferFromFile(&save, savename) == false)
{
CONS_Printf(M_GetText("Couldn't read file %s\n"), savename);
return;
}
length = save.size;
{
char temp[sizeof(timeattackfolder)];
UINT8 *lives_p;
SINT8 pllives;
save.p = save.buffer;
save.size = length;
save.end = save.buffer + save.size;
// Version check
memset(vcheck, 0, sizeof (vcheck));
sprintf(vcheck, (marathonmode ? "back-up %d" : "version %d"), VERSION);
@ -4895,9 +4761,8 @@ cleanup:
CONS_Printf(M_GetText("Game saved.\n"));
else if (!saved)
CONS_Alert(CONS_ERROR, M_GetText("Error while writing to %s for save slot %u, base: %s\n"), backup, slot, (marathonmode ? liveeventbackup : savegamename));
Z_Free(save.buffer);
save.p = save.buffer = NULL;
P_SaveBufferFree(&save);
}
#undef CHECKPOS
#undef BADSAVE
@ -4910,7 +4775,6 @@ cleanup:
void G_DeferedInitNew(boolean pencoremode, INT32 map, INT32 pickedchar, UINT8 ssplayers, boolean FLS)
{
UINT16 color = SKINCOLOR_NONE;
INT32 dogametype;
paused = false;
@ -4921,17 +4785,8 @@ void G_DeferedInitNew(boolean pencoremode, INT32 map, INT32 pickedchar, UINT8 ss
G_ResetRandMapBuffer();
if ((modeattacking == ATTACKING_CAPSULES) || (bossinfo.boss == true))
{
dogametype = GT_BATTLE;
}
else
{
dogametype = GT_RACE;
}
// this leave the actual game if needed
SV_StartSinglePlayerServer(dogametype, false);
SV_StartSinglePlayerServer(gametype, false);
if (splitscreen != ssplayers)
{
@ -5031,7 +4886,7 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr
automapactive = false;
imcontinuing = false;
if ((gametyperules & GTR_CAMPAIGN) && !skipprecutscene && mapheaderinfo[gamemap-1]->precutscenenum && !modeattacking && !(marathonmode & MA_NOCUTSCENES)) // Start a custom cutscene.
if ((grandprixinfo.gp == true) && !skipprecutscene && mapheaderinfo[gamemap-1]->precutscenenum && !modeattacking && !(marathonmode & MA_NOCUTSCENES)) // Start a custom cutscene.
F_StartCustomCutscene(mapheaderinfo[gamemap-1]->precutscenenum-1, true, resetplayer);
else
{

View file

@ -183,24 +183,17 @@ void G_SaveGame(UINT32 slot, INT16 mapnum);
void G_SaveGameOver(UINT32 slot, boolean modifylives);
extern UINT32 gametypedefaultrules[NUMGAMETYPES];
extern UINT32 gametypetol[NUMGAMETYPES];
extern INT16 gametyperankings[NUMGAMETYPES];
void G_SetGametype(INT16 gametype);
INT16 G_AddGametype(UINT32 rules);
void G_AddGametypeConstant(INT16 gtype, const char *newgtconst);
void G_UpdateGametypeSelections(void);
char *G_PrepareGametypeConstant(const char *newgtconst);
void G_AddTOL(UINT32 newtol, const char *tolname);
void G_AddGametypeTOL(INT16 gtype, UINT32 newtol);
INT32 G_GetGametypeByName(const char *gametypestr);
boolean G_IsSpecialStage(INT32 mapnum);
INT32 G_GuessGametypeByTOL(UINT32 tol);
boolean G_GametypeUsesLives(void);
boolean G_GametypeHasTeams(void);
boolean G_GametypeHasSpectators(void);
#define VOTEMODIFIER_ENCORE 0x80
INT16 G_SometimesGetDifferentGametype(UINT8 prefgametype);
UINT8 G_GetGametypeColor(INT16 gt);
INT16 G_SometimesGetDifferentGametype(void);
void G_ExitLevel(void);
void G_NextLevel(void);
void G_Continue(void);

View file

@ -16,7 +16,7 @@
#include "hu_stuff.h"
#include "font.h"
#include "k_menu.h" // gametype_cons_t
#include "k_menu.h" // highlightflags
#include "m_cond.h" // emblems
#include "m_misc.h" // word jumping
@ -55,7 +55,8 @@
// SRB2Kart
#include "s_sound.h" // song credits
#include "k_kart.h"
#include "k_boss.h"
#include "k_battle.h"
#include "k_grandprix.h"
#include "k_color.h"
#include "k_hud.h"
#include "r_fps.h"
@ -2392,60 +2393,110 @@ static inline void HU_DrawSpectatorTicker(void)
static void HU_DrawRankings(void)
{
playersort_t tab[MAXPLAYERS];
INT32 i, j, scorelines, hilicol, numplayersingame = 0;
INT32 i, j, scorelines, numplayersingame = 0, hilicol = highlightflags;
boolean completed[MAXPLAYERS];
UINT32 whiteplayer = MAXPLAYERS;
boolean timedone = false, pointsdone = false;
V_DrawFadeScreen(0xFF00, 16); // A little more readable, and prevents cheating the fades under other circumstances.
if (modeattacking)
hilicol = V_ORANGEMAP;
else
hilicol = ((gametype == GT_RACE) ? V_SKYMAP : V_REDMAP);
// draw the current gametype in the lower right
if (modeattacking)
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Record Attack");
else
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, Gametype_Names[gametype]);
if (grandprixinfo.gp == true)
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Grand Prix");
else if (battlecapsules)
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, "Capsules");
else if (gametype >= 0 && gametype < numgametypes)
V_DrawString(4, 188, hilicol|V_SNAPTOBOTTOM|V_SNAPTOLEFT, gametypes[gametype]->name);
if ((gametyperules & (GTR_TIMELIMIT|GTR_POINTLIMIT)) && !bossinfo.boss)
// Left hand side
if (grandprixinfo.gp == true)
{
if ((gametyperules & GTR_TIMELIMIT) && timelimitintics > 0)
const char *roundstr = NULL;
V_DrawCenteredString(64, 8, 0, "ROUND");
switch (grandprixinfo.eventmode)
{
UINT32 timeval = (timelimitintics + starttime + 1 - leveltime);
if (timeval > timelimitintics+1)
timeval = timelimitintics+1;
timeval /= TICRATE;
case GPEVENT_BONUS:
roundstr = "BONUS";
break;
case GPEVENT_SPECIAL:
roundstr = "SPECIAL";
break;
default:
roundstr = va("%d", grandprixinfo.roundnum);
break;
}
V_DrawCenteredString(64, 16, hilicol, roundstr);
}
else if ((gametyperules & GTR_TIMELIMIT) && timelimitintics > 0)
{
UINT32 timeval = (timelimitintics + starttime + 1 - leveltime);
if (timeval > timelimitintics+1)
timeval = timelimitintics+1;
timeval /= TICRATE;
if (leveltime <= (timelimitintics + starttime))
{
V_DrawCenteredString(64, 8, 0, "TIME LEFT");
V_DrawCenteredString(64, 16, hilicol, va("%u", timeval));
}
// overtime
if (!players[consoleplayer].exiting && (leveltime > (timelimitintics + starttime + TICRATE/2)) && cv_overtime.value)
{
V_DrawCenteredString(64, 8, 0, "TIME LEFT");
V_DrawCenteredString(64, 16, hilicol, "OVERTIME");
}
if (leveltime <= (timelimitintics + starttime))
{
V_DrawCenteredString(64, 8, 0, "TIME LEFT");
V_DrawCenteredString(64, 16, hilicol, va("%u", timeval));
}
if ((gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0)
// overtime
if (!players[consoleplayer].exiting && (leveltime > (timelimitintics + starttime + TICRATE/2)) && cv_overtime.value)
{
V_DrawCenteredString(256, 8, 0, "POINT LIMIT");
V_DrawCenteredString(256, 16, hilicol, va("%d", cv_pointlimit.value));
V_DrawCenteredString(64, 8, 0, "TIME LEFT");
V_DrawCenteredString(64, 16, hilicol, "OVERTIME");
}
timedone = true;
}
else if ((gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0)
{
V_DrawCenteredString(64, 8, 0, "POINT LIMIT");
V_DrawCenteredString(64, 16, hilicol, va("%d", cv_pointlimit.value));
pointsdone = true;
}
else if (gametyperules & GTR_CIRCUIT)
{
V_DrawCenteredString(64, 8, 0, "LAPS");
V_DrawCenteredString(64, 16, hilicol, va("%d", numlaps));
}
// Right hand side
if (battlecapsules == true)
{
if (numtargets < maptargets)
{
V_DrawCenteredString(256, 8, 0, "CAPSULES");
V_DrawCenteredString(256, 16, hilicol, va("%d", maptargets - numtargets));
}
}
else
else if (!timedone && (gametyperules & GTR_TIMELIMIT) && timelimitintics > 0)
{
if (circuitmap)
UINT32 timeval = (timelimitintics + starttime + 1 - leveltime);
if (timeval > timelimitintics+1)
timeval = timelimitintics+1;
timeval /= TICRATE;
if (leveltime <= (timelimitintics + starttime))
{
V_DrawCenteredString(64, 8, 0, "LAP COUNT");
V_DrawCenteredString(64, 16, hilicol, va("%d", numlaps));
V_DrawCenteredString(256, 8, 0, "TIME LEFT");
V_DrawCenteredString(256, 16, hilicol, va("%u", timeval));
}
// overtime
if (!players[consoleplayer].exiting && (leveltime > (timelimitintics + starttime + TICRATE/2)) && cv_overtime.value)
{
V_DrawCenteredString(256, 8, 0, "TIME LEFT");
V_DrawCenteredString(256, 16, hilicol, "OVERTIME");
}
}
else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0)
{
V_DrawCenteredString(256, 8, 0, "POINT LIMIT");
V_DrawCenteredString(256, 16, hilicol, va("%d", cv_pointlimit.value));
}
else if (gametyperules & GTR_CIRCUIT)
{
V_DrawCenteredString(256, 8, 0, "GAME SPEED");
V_DrawCenteredString(256, 16, hilicol, kartspeed_cons_t[1+gamespeed].strvalue);
}
@ -2494,13 +2545,12 @@ static void HU_DrawRankings(void)
if ((gametyperules & GTR_CIRCUIT))
{
if (circuitmap)
tab[scorelines].count = players[i].laps;
else
tab[scorelines].count = players[i].realtime;
tab[scorelines].count = players[i].laps;
}
else
{
tab[scorelines].count = players[i].roundscore;
}
scorelines++;

View file

@ -548,6 +548,9 @@ char sprnames[NUMSPRITES + 1][5] =
"MGBX", // Heavy Magician transform box
"MGBT", // Heavy Magician transform box top
"MGBB", // Heavy Magician transform box bottom
"MSHD", // Item Monitor Big Shard
"IMDB", // Item Monitor Small Shard (Debris)
"MTWK", // Item Monitor Glass Twinkle
"WIPD", // Wipeout dust trail
"DRIF", // Drift Sparks
@ -789,6 +792,8 @@ char sprnames[NUMSPRITES + 1][5] =
"UFOA",
"UFOS",
"UQMK",
// First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later
"VIEW",
};
@ -3900,6 +3905,24 @@ state_t states[NUMSTATES] =
//{SPR_ICAP, FF_FLOORSPRITE|4, -1, {NULL}, 0, 0, S_NULL}, // S_ITEMCAPSULE_BOTTOM
//{SPR_ICAP, FF_FLOORSPRITE|5, -1, {NULL}, 0, 0, S_NULL}, // S_ITEMCAPSULE_INSIDE
{SPR_NULL, 0, 1, {NULL}, 6, 1, S_SPAWNSTATE}, // S_MONITOR_DAMAGE
{SPR_NULL, 0, 1, {NULL}, 0, 0, S_NULL}, // S_MONITOR_DEATH
{SPR_IMON, FF_PAPERSPRITE|1, 1, {NULL}, 3, 1, S_MONITOR_SCREEN1B}, // S_MONITOR_SCREEN1A
{SPR_IMON, FF_PAPERSPRITE|0, 1, {NULL}, 3, 1, S_MONITOR_SCREEN2A}, // S_MONITOR_SCREEN1B
{SPR_IMON, FF_PAPERSPRITE|2, 1, {NULL}, 3, 1, S_MONITOR_SCREEN2B}, // S_MONITOR_SCREEN2A
{SPR_IMON, FF_PAPERSPRITE|0, 1, {NULL}, 3, 1, S_MONITOR_SCREEN3A}, // S_MONITOR_SCREEN2B
{SPR_IMON, FF_PAPERSPRITE|3, 1, {NULL}, 3, 1, S_MONITOR_SCREEN3B}, // S_MONITOR_SCREEN3A
{SPR_IMON, FF_PAPERSPRITE|0, 1, {NULL}, 3, 1, S_MONITOR_SCREEN4A}, // S_MONITOR_SCREEN3B
{SPR_IMON, FF_PAPERSPRITE|4, 1, {NULL}, 3, 1, S_MONITOR_SCREEN4B}, // S_MONITOR_SCREEN4A
{SPR_IMON, FF_PAPERSPRITE|0, 1, {NULL}, 3, 1, S_MONITOR_SCREEN1A}, // S_MONITOR_SCREEN4B
{SPR_IMON, FF_PAPERSPRITE|5, -1, {NULL}, 3, 1, S_NULL}, // S_MONITOR_STAND
{SPR_NULL, FF_PAPERSPRITE|FF_TRANS50|FF_ADD|6, -1, {NULL}, 3, 35, S_NULL}, // S_MONITOR_CRACKA
{SPR_NULL, FF_PAPERSPRITE|FF_TRANS50|FF_SUBTRACT|10, -1, {NULL}, 3, 35, S_NULL}, // S_MONITOR_CRACKB
{SPR_MSHD, FF_FULLBRIGHT|FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 7, 2, S_NULL}, // S_MONITOR_BIG_SHARD
{SPR_IMDB, FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_MONITOR_SMALL_SHARD
{SPR_MTWK, FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_MONITOR_TWINKLE
{SPR_MGBX, FF_PAPERSPRITE|0, -1, {NULL}, 0, 0, S_NULL}, // S_MAGICIANBOX
{SPR_MGBT, FF_FLOORSPRITE|0, -1, {NULL}, 0, 0, S_NULL}, // S_MAGICIANBOX_TOP
{SPR_MGBB, FF_FLOORSPRITE|0, -1, {NULL}, 0, 0, S_NULL}, // S_MAGICIANBOX_BOTTOM
@ -22420,6 +22443,87 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
S_NULL // raisestate
},
{ // MT_MONITOR
-1, // doomednum
S_INVISIBLE, // spawnstate
FRACUNIT, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_MONITOR_DAMAGE, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_MONITOR_DEATH, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
32*FRACUNIT, // radius
112*FRACUNIT, // height
0, // display offset
100, // mass
0, // damage
sfx_None, // activesound
MF_SOLID|MF_SHOOTABLE|MF_SLIDEME|MF_DONTENCOREMAP|MF_NOHITLAGFORME, // flags
S_NULL // raisestate
},
{ // MT_MONITOR_PART
-1, // doomednum
S_INVISIBLE, // spawnstate
1, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
32*FRACUNIT, // radius
112*FRACUNIT, // height
0, // display offset
100, // mass
0, // damage
sfx_None, // activesound
MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPTHING|MF_NOCLIPHEIGHT|MF_NOSQUISH, // flags
S_NULL // raisestate
},
{ // MT_MONITOR_SHARD
-1, // doomednum
S_MONITOR_BIG_SHARD, // spawnstate
1, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
0, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
32*FRACUNIT, // radius
32*FRACUNIT, // height
0, // display offset
100, // mass
0, // damage
sfx_None, // activesound
MF_SCENERY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPTHING|MF_NOSQUISH, // flags
S_NULL // raisestate
},
{ // MT_MAGICIANBOX
-1, // doomednum
S_MAGICIANBOX, // spawnstate

View file

@ -1099,6 +1099,9 @@ typedef enum sprite
SPR_MGBX, // Heavy Magician transform box
SPR_MGBT, // Heavy Magician transform box top
SPR_MGBB, // Heavy Magician transform box bottom
SPR_MSHD, // Item Monitor Big Shard
SPR_IMDB, // Item Monitor Small Shard (Debris)
SPR_MTWK, // Item Monitor Glass Twinkle
SPR_WIPD, // Wipeout dust trail
SPR_DRIF, // Drift Sparks
@ -1340,6 +1343,8 @@ typedef enum sprite
SPR_UFOA,
SPR_UFOS,
SPR_UQMK,
// First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later
SPR_VIEW,
@ -4309,6 +4314,24 @@ typedef enum state
//S_ITEMCAPSULE_BOTTOM,
//S_ITEMCAPSULE_INSIDE,
S_MONITOR_DAMAGE,
S_MONITOR_DEATH,
S_MONITOR_SCREEN1A,
S_MONITOR_SCREEN1B,
S_MONITOR_SCREEN2A,
S_MONITOR_SCREEN2B,
S_MONITOR_SCREEN3A,
S_MONITOR_SCREEN3B,
S_MONITOR_SCREEN4A,
S_MONITOR_SCREEN4B,
S_MONITOR_STAND,
S_MONITOR_CRACKA,
S_MONITOR_CRACKB,
S_MONITOR_BIG_SHARD,
S_MONITOR_SMALL_SHARD,
S_MONITOR_TWINKLE,
S_MAGICIANBOX,
S_MAGICIANBOX_TOP,
S_MAGICIANBOX_BOTTOM,
@ -6360,6 +6383,9 @@ typedef enum mobj_type
MT_FLOATINGITEM,
MT_ITEMCAPSULE,
MT_ITEMCAPSULE_PART,
MT_MONITOR,
MT_MONITOR_PART,
MT_MONITOR_SHARD,
MT_MAGICIANBOX,
MT_SIGNSPARKLE,

View file

@ -1,3 +1,12 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "streams.hpp"
template class srb2::io::ZlibInputStream<srb2::io::SpanStream>;

View file

@ -1,3 +1,12 @@
// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// Copyright (C) 2022-2023 by Ronald "Eidolon" Kinard
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#ifndef __SRB2_IO_STREAMS_HPP__
#define __SRB2_IO_STREAMS_HPP__
@ -409,7 +418,7 @@ public:
switch (seek_from) {
case SeekFrom::kStart:
if (offset < 0 || offset >= static_cast<StreamOffset>(span_.size())) {
if (offset < 0) {
throw std::logic_error("start offset is out of bounds");
}
head = offset;
@ -421,7 +430,7 @@ public:
head = span_.size() - offset;
break;
case SeekFrom::kCurrent:
if (head_ + offset < 0 || head_ + offset >= span_.size()) {
if (head_ + offset < 0) {
throw std::logic_error("offset is out of bounds");
}
head = head_ + offset;
@ -480,7 +489,7 @@ public:
switch (seek_from) {
case SeekFrom::kStart:
if (offset < 0 || offset >= static_cast<StreamOffset>(vec_.size())) {
if (offset < 0) {
throw std::logic_error("start offset is out of bounds");
}
head = offset;
@ -492,7 +501,7 @@ public:
head = vec_.size() - offset;
break;
case SeekFrom::kCurrent:
if (head_ + offset < 0 || head_ + offset >= vec_.size()) {
if (head_ + offset < 0) {
throw std::logic_error("offset is out of bounds");
}
head = head_ + offset;

View file

@ -2,7 +2,6 @@
/// \brief SRB2Kart Battle Mode specific code
#include "k_battle.h"
#include "k_boss.h"
#include "k_kart.h"
#include "doomtype.h"
#include "doomdata.h"
@ -19,6 +18,7 @@
#include "r_sky.h" // skyflatnum
#include "k_grandprix.h" // K_CanChangeRules
#include "p_spec.h"
#include "k_objects.h"
// Battle overtime info
struct battleovertime battleovertime;
@ -146,7 +146,7 @@ void K_CheckBumpers(void)
}
else if (numingame <= 1)
{
if (!battlecapsules)
if ((gametyperules & GTR_CAPSULES) && !battlecapsules)
{
// Reset map to turn on battle capsules
if (server)
@ -196,6 +196,11 @@ void K_CheckEmeralds(player_t *player)
{
UINT8 i;
if (!(gametyperules & GTR_POWERSTONES))
{
return;
}
if (!ALLCHAOSEMERALDS(player->emeralds))
{
return;
@ -221,6 +226,29 @@ void K_CheckEmeralds(player_t *player)
K_CheckBumpers();
}
UINT16 K_GetChaosEmeraldColor(UINT32 emeraldType)
{
switch (emeraldType)
{
case EMERALD_CHAOS1:
return SKINCOLOR_CHAOSEMERALD1;
case EMERALD_CHAOS2:
return SKINCOLOR_CHAOSEMERALD2;
case EMERALD_CHAOS3:
return SKINCOLOR_CHAOSEMERALD3;
case EMERALD_CHAOS4:
return SKINCOLOR_CHAOSEMERALD4;
case EMERALD_CHAOS5:
return SKINCOLOR_CHAOSEMERALD5;
case EMERALD_CHAOS6:
return SKINCOLOR_CHAOSEMERALD6;
case EMERALD_CHAOS7:
return SKINCOLOR_CHAOSEMERALD7;
default:
return SKINCOLOR_NONE;
}
}
mobj_t *K_SpawnChaosEmerald(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 flip, UINT32 emeraldType)
{
boolean validEmerald = true;
@ -240,25 +268,13 @@ mobj_t *K_SpawnChaosEmerald(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT
switch (emeraldType)
{
case EMERALD_CHAOS1:
emerald->color = SKINCOLOR_CHAOSEMERALD1;
break;
case EMERALD_CHAOS2:
emerald->color = SKINCOLOR_CHAOSEMERALD2;
break;
case EMERALD_CHAOS3:
emerald->color = SKINCOLOR_CHAOSEMERALD3;
break;
case EMERALD_CHAOS4:
emerald->color = SKINCOLOR_CHAOSEMERALD4;
break;
case EMERALD_CHAOS5:
emerald->color = SKINCOLOR_CHAOSEMERALD5;
break;
case EMERALD_CHAOS6:
emerald->color = SKINCOLOR_CHAOSEMERALD6;
break;
case EMERALD_CHAOS7:
emerald->color = SKINCOLOR_CHAOSEMERALD7;
emerald->color = K_GetChaosEmeraldColor(emeraldType);
break;
default:
CONS_Printf("Invalid emerald type %d\n", emeraldType);
@ -336,12 +352,17 @@ UINT8 K_NumEmeralds(player_t *player)
return num;
}
static inline boolean IsOnInterval(tic_t interval)
{
return ((leveltime - starttime) % interval) == 0;
}
void K_RunPaperItemSpawners(void)
{
const boolean overtime = (battleovertime.enabled >= 10*TICRATE);
tic_t interval = 8*TICRATE;
const tic_t interval = BATTLE_SPAWN_INTERVAL;
const boolean canmakeemeralds = true; //(!(battlecapsules || bossinfo.boss));
const boolean canmakeemeralds = (gametyperules & GTR_POWERSTONES);
UINT32 emeraldsSpawned = 0;
UINT32 firstUnspawnedEmerald = 0;
@ -352,7 +373,7 @@ void K_RunPaperItemSpawners(void)
UINT8 pcount = 0;
INT16 i;
if (battlecapsules || bossinfo.boss)
if (battlecapsules)
{
// Gametype uses paper items, but this specific expression doesn't
return;
@ -364,13 +385,7 @@ void K_RunPaperItemSpawners(void)
return;
}
if (overtime == true)
{
// Double frequency of items
interval /= 2;
}
if (((leveltime - starttime) % interval) != 0)
if (!IsOnInterval(interval))
{
return;
}
@ -458,9 +473,7 @@ void K_RunPaperItemSpawners(void)
#define MAXITEM 64
mobj_t *spotList[MAXITEM];
UINT8 spotMap[MAXITEM];
UINT8 spotCount = 0, spotBackup = 0;
INT16 starti = 0;
UINT8 spotCount = 0, spotBackup = 0, spotAvailable = 0;
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
{
@ -474,14 +487,26 @@ void K_RunPaperItemSpawners(void)
emeraldsSpawned |= mo->extravalue1;
}
if (mo->type == MT_MONITOR)
{
emeraldsSpawned |= Obj_MonitorGetEmerald(mo);
}
if (mo->type != MT_PAPERITEMSPOT)
continue;
if (spotCount >= MAXITEM)
continue;
if (Obj_ItemSpotIsAvailable(mo))
{
// spotMap first only includes spots
// where a monitor doesn't exist
spotMap[spotAvailable] = spotCount;
spotAvailable++;
}
spotList[spotCount] = mo;
spotMap[spotCount] = spotCount;
spotCount++;
}
@ -499,7 +524,6 @@ void K_RunPaperItemSpawners(void)
if (!(emeraldsSpawned & emeraldFlag))
{
firstUnspawnedEmerald = emeraldFlag;
starti = -1;
break;
}
}
@ -507,77 +531,72 @@ void K_RunPaperItemSpawners(void)
//CONS_Printf("leveltime = %d ", leveltime);
spotBackup = spotCount;
for (i = starti; i < pcount; i++)
if (spotAvailable > 0)
{
UINT8 r = 0, key = 0;
mobj_t *drop = NULL;
SINT8 flip = 1;
const UINT8 r = spotMap[P_RandomKey(PR_ITEM_ROULETTE, spotAvailable)];
if (spotCount == 0)
Obj_ItemSpotAssignMonitor(spotList[r], Obj_SpawnMonitor(
spotList[r], 1 + pcount, firstUnspawnedEmerald));
}
for (i = 0; i < spotCount; ++i)
{
// now spotMap includes every spot
spotMap[i] = i;
}
if ((gametyperules & GTR_SPHERES) && IsOnInterval(2 * interval))
{
spotBackup = spotCount;
for (i = 0; i < pcount; i++)
{
// all are accessible again
spotCount = spotBackup;
}
UINT8 r = 0, key = 0;
mobj_t *drop = NULL;
SINT8 flip = 1;
if (spotCount == 1)
{
key = 0;
}
else
{
key = P_RandomKey(PR_ITEM_ROULETTE, spotCount);
}
r = spotMap[key];
//CONS_Printf("[%d %d %d] ", i, key, r);
flip = P_MobjFlip(spotList[r]);
// When -1, we're spawning a Chaos Emerald.
if (i == -1)
{
drop = K_SpawnChaosEmerald(
spotList[r]->x, spotList[r]->y, spotList[r]->z + (128 * mapobjectscale * flip),
FixedAngle(P_RandomRange(PR_ITEM_ROULETTE, 0, 359) * FRACUNIT), flip,
firstUnspawnedEmerald
);
}
else
{
if (gametyperules & GTR_SPHERES)
if (spotCount == 0)
{
drop = K_SpawnSphereBox(
spotList[r]->x, spotList[r]->y, spotList[r]->z + (128 * mapobjectscale * flip),
FixedAngle(P_RandomRange(PR_ITEM_ROULETTE, 0, 359) * FRACUNIT), flip,
10
);
K_FlipFromObject(drop, spotList[r]);
// all are accessible again
spotCount = spotBackup;
}
drop = K_CreatePaperItem(
if (spotCount == 1)
{
key = 0;
}
else
{
key = P_RandomKey(PR_ITEM_ROULETTE, spotCount);
}
r = spotMap[key];
//CONS_Printf("[%d %d %d] ", i, key, r);
flip = P_MobjFlip(spotList[r]);
drop = K_SpawnSphereBox(
spotList[r]->x, spotList[r]->y, spotList[r]->z + (128 * mapobjectscale * flip),
FixedAngle(P_RandomRange(PR_ITEM_ROULETTE, 0, 359) * FRACUNIT), flip,
0, 0
FixedAngle(P_RandomRange(PR_ITEM_ROULETTE, 0, 359) * FRACUNIT), flip,
10
);
}
K_FlipFromObject(drop, spotList[r]);
K_FlipFromObject(drop, spotList[r]);
spotCount--;
if (key != spotCount)
{
// So the core theory of what's going on is that we keep every
// available option at the front of the array, so we don't have
// to skip over any gaps or do recursion to avoid doubles.
// But because spotCount can be reset in the case of a low
// quanitity of item spawnpoints in a map, we still need every
// entry in the array, even outside of the "visible" range.
// A series of swaps allows us to adhere to both constraints.
// -toast 22/03/22 (semipalindromic!)
spotMap[key] = spotMap[spotCount];
spotMap[spotCount] = r; // was set to spotMap[key] previously
spotCount--;
if (key != spotCount)
{
// So the core theory of what's going on is that we keep every
// available option at the front of the array, so we don't have
// to skip over any gaps or do recursion to avoid doubles.
// But because spotCount can be reset in the case of a low
// quanitity of item spawnpoints in a map, we still need every
// entry in the array, even outside of the "visible" range.
// A series of swaps allows us to adhere to both constraints.
// -toast 22/03/22 (semipalindromic!)
spotMap[key] = spotMap[spotCount];
spotMap[spotCount] = r; // was set to spotMap[key] previously
}
}
}
//CONS_Printf("\n");
@ -789,7 +808,7 @@ void K_BattleInit(boolean singleplayercontext)
{
size_t i;
if ((gametyperules & GTR_CAPSULES) && singleplayercontext && !battlecapsules && !bossinfo.boss)
if ((gametyperules & GTR_CAPSULES) && singleplayercontext && !battlecapsules)
{
mapthing_t *mt = mapthings;
for (i = 0; i < nummapthings; i++, mt++)

View file

@ -8,6 +8,9 @@
extern "C" {
#endif
#define BATTLE_SPAWN_INTERVAL (4*TICRATE)
#define BATTLE_DESPAWN_TIME (15*TICRATE)
extern struct battleovertime
{
UINT16 enabled; ///< Has this been initalized yet?
@ -25,6 +28,7 @@ boolean K_IsPlayerWanted(player_t *player);
void K_SpawnBattlePoints(player_t *source, player_t *victim, UINT8 amount);
void K_CheckBumpers(void);
void K_CheckEmeralds(player_t *player);
UINT16 K_GetChaosEmeraldColor(UINT32 emeraldType);
mobj_t *K_SpawnChaosEmerald(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 flip, UINT32 emeraldType);
mobj_t *K_SpawnSphereBox(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 flip, UINT8 amount);
void K_DropEmeraldsFromPlayer(player_t *player, UINT32 emeraldType);

View file

@ -23,7 +23,7 @@
struct bossinfo bossinfo;
/*--------------------------------------------------
void K_ClearBossInfo(void)
void K_ResetBossInfo(void)
See header file for description.
--------------------------------------------------*/
@ -32,6 +32,8 @@ void K_ResetBossInfo(void)
Z_Free(bossinfo.enemyname);
Z_Free(bossinfo.subtitle);
memset(&bossinfo, 0, sizeof(struct bossinfo));
bossinfo.barlen = BOSSHEALTHBARLEN;
bossinfo.titlesound = sfx_typri1;
}
/*--------------------------------------------------
@ -43,7 +45,7 @@ void K_BossInfoTicker(void)
{
UINT8 i;
if (bossinfo.boss == false)
if (bossinfo.valid == false)
return;
// Update healthbar data. (only if the hud is visible)
@ -55,7 +57,7 @@ void K_BossInfoTicker(void)
bossinfo.visualbar--;
// If the boss is dying, start shrinking the healthbar.
if (bossinfo.visualbar == 0)
bossinfo.barlen-= 2;
bossinfo.barlen -= 2;
}
// Less than the actual health?
else if (bossinfo.visualbar < bossinfo.healthbar)
@ -108,6 +110,18 @@ void K_BossInfoTicker(void)
void K_InitBossHealthBar(const char *enemyname, const char *subtitle, sfxenum_t titlesound, fixed_t pinchmagnitude, UINT8 divisions)
{
if (!(gametyperules & GTR_BOSS))
{
return;
}
bossinfo.valid = true;
if (!leveltime)
{
bossinfo.coolintro = true;
}
if (enemyname && enemyname[0])
{
Z_Free(bossinfo.enemyname);
@ -158,6 +172,9 @@ void K_InitBossHealthBar(const char *enemyname, const char *subtitle, sfxenum_t
void K_UpdateBossHealthBar(fixed_t magnitude, tic_t jitterlen)
{
if (bossinfo.valid == false)
return;
if (magnitude > FRACUNIT)
magnitude = FRACUNIT;
else if (magnitude < 0)
@ -177,6 +194,9 @@ void K_DeclareWeakspot(mobj_t *spot, spottype_t spottype, UINT16 color, boolean
{
UINT8 i;
if (bossinfo.valid == false)
return;
// First check whether the spot is already in the list and simply redeclaring weakness (for example, a vulnerable moment in the pattern).
for (i = 0; i < NUMWEAKSPOTS; i++)
if (bossinfo.weakspots[i].spot == spot)
@ -206,3 +226,16 @@ void K_DeclareWeakspot(mobj_t *spot, spottype_t spottype, UINT16 color, boolean
bossinfo.doweakspotsound = spottype;
}
}
/*--------------------------------------------------
boolean K_CheckBossIntro(void);
See header file for description.
--------------------------------------------------*/
boolean K_CheckBossIntro(void)
{
if (bossinfo.valid == false)
return false;
return bossinfo.coolintro;
}

View file

@ -43,7 +43,8 @@ struct weakspot_t
extern struct bossinfo
{
boolean boss; ///< If true, then we are fighting a boss
boolean valid; ///< If true, then data in this struct is valid
UINT8 healthbar; ///< Actual health bar fill amount
UINT8 visualbar; ///< Tracks above, but with delay
fixed_t visualdiv; ///< How far apart health bar divisions should appear
@ -52,7 +53,7 @@ extern struct bossinfo
UINT8 barlen; ///< The length of the bar (only reduced when a boss is deceased)
char *enemyname; ///< The name next to the bar
weakspot_t weakspots[NUMWEAKSPOTS]; ///< Array of weak spots (for minimap/object tracking)
boolean encore; ///< Copy of encore, just to make sure you can't cheat it with cvars
boolean coolintro; ///< Determines whether the map start(s/ed) with a boss-specific intro.
spottype_t doweakspotsound; ///< If nonzero, at least one weakspot was declared this tic
tic_t titleshow; ///< Show this many letters on the titlecard
sfxenum_t titlesound; ///< Sound to play when title typing
@ -116,6 +117,18 @@ void K_UpdateBossHealthBar(fixed_t magnitude, tic_t jitterlen);
void K_DeclareWeakspot(mobj_t *spot, spottype_t spottype, UINT16 color, boolean minimap);
/*--------------------------------------------------
boolean K_CheckBossIntro(void);
Checks whether the Versus-specific intro is playing for this map start.
Return:-
true if cool intro in action,
otherwise false.
--------------------------------------------------*/
boolean K_CheckBossIntro(void);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -27,7 +27,6 @@
#include "m_random.h"
#include "r_things.h" // numskins
#include "k_race.h" // finishBeamLine
#include "k_boss.h"
#include "m_perfstats.h"
@ -168,7 +167,7 @@ void K_UpdateMatchRaceBots(void)
}
}
if (difficulty == 0 || !(gametyperules & GTR_BOTS) || bossinfo.boss == true)
if (difficulty == 0 || !(gametyperules & GTR_BOTS))
{
wantedbots = 0;
}

View file

@ -324,6 +324,11 @@ tic_t K_MineExplodeAttack(mobj_t *actor, fixed_t size, boolean spin)
if (!spin)
{
if (minehitlag == 0)
{
minehitlag = actor->hitlag;
}
Obj_SpawnBrolyKi(actor, minehitlag);
return minehitlag;

View file

@ -11,7 +11,6 @@
/// \brief Grand Prix mode game logic & bot behaviors
#include "k_grandprix.h"
#include "k_boss.h"
#include "k_specialstage.h"
#include "doomdef.h"
#include "d_player.h"
@ -339,7 +338,7 @@ void K_UpdateGrandPrixBots(void)
continue;
}
players[i].spectator = (grandprixinfo.eventmode != GPEVENT_NONE);
players[i].spectator = !(gametyperules & GTR_BOTS) || (grandprixinfo.eventmode != GPEVENT_NONE);
}
// Find the rival.
@ -529,7 +528,7 @@ void K_RetireBots(void)
UINT8 i;
if (grandprixinfo.gp == true
&& ((grandprixinfo.roundnum >= grandprixinfo.cup->numlevels)
&& ((grandprixinfo.cup != NULL && grandprixinfo.roundnum >= grandprixinfo.cup->numlevels)
|| grandprixinfo.eventmode != GPEVENT_NONE))
{
// No replacement.
@ -676,7 +675,7 @@ void K_PlayerLoseLife(player_t *player)
return;
}
if (player->spectator || player->exiting || player->bot || player->lives <= 0 || (player->pflags & PF_LOSTLIFE))
if (player->spectator || (player->exiting && !(player->pflags & PF_NOCONTEST)) || player->bot || player->lives <= 0 || (player->pflags & PF_LOSTLIFE))
{
return;
}
@ -703,24 +702,12 @@ void K_PlayerLoseLife(player_t *player)
--------------------------------------------------*/
boolean K_CanChangeRules(boolean allowdemos)
{
if (grandprixinfo.gp == true && grandprixinfo.roundnum > 0)
if (grandprixinfo.gp == true /*&& grandprixinfo.roundnum > 0*/)
{
// Don't cheat the rules of the GP!
return false;
}
if (bossinfo.boss == true)
{
// Don't cheat the boss!
return false;
}
if (specialStage.active == true)
{
// Don't cheat special stages!
return false;
}
if (marathonmode)
{
// Don't cheat the endurance challenge!

View file

@ -785,6 +785,29 @@ static patch_t *K_GetCachedItemPatch(INT32 item, UINT8 offset)
return NULL;
}
static patch_t *K_GetSmallStaticCachedItemPatch(kartitems_t item)
{
UINT8 offset;
item = K_ItemResultToType(item);
switch (item)
{
case KITEM_INVINCIBILITY:
offset = 7;
break;
case KITEM_ORBINAUT:
offset = 4;
break;
default:
offset = 1;
}
return K_GetCachedItemPatch(item, offset);
}
//}
INT32 ITEM_X, ITEM_Y; // Item Window
@ -1863,7 +1886,7 @@ static boolean K_drawKartPositionFaces(void)
ranklines--;
i = ranklines;
if (gametype == GT_BATTLE || strank <= 2) // too close to the top, or playing battle, or a spectator? would have had (strank == -1) called out, but already caught by (strank <= 2)
if ((gametyperules & GTR_POINTLIMIT) || strank <= 2) // too close to the top, or playing battle, or a spectator? would have had (strank == -1) called out, but already caught by (strank <= 2)
{
if (i > 4) // could be both...
i = 4;
@ -1951,7 +1974,7 @@ static boolean K_drawKartPositionFaces(void)
if (i == strank)
V_DrawScaledPatch(FACE_X, Y, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_facehighlight[(leveltime / 4) % 8]);
if (gametype == GT_BATTLE && players[rankplayer[i]].bumpers <= 0)
if ((gametyperules & GTR_BUMPERS) && players[rankplayer[i]].bumpers <= 0)
V_DrawScaledPatch(FACE_X-4, Y-3, V_HUDTRANS|V_SLIDEIN|V_SNAPTOLEFT, kp_ranknobumpers);
else
{
@ -1973,7 +1996,7 @@ static void K_drawBossHealthBar(void)
UINT8 i = 0, barstatus = 1, randlen = 0, darken = 0;
const INT32 startx = BASEVIDWIDTH - 23;
INT32 starty = BASEVIDHEIGHT - 25;
INT32 rolrand = 0;
INT32 rolrand = 0, randtemp = 0;
boolean randsign = false;
if (bossinfo.barlen <= 1)
@ -2019,7 +2042,9 @@ static void K_drawBossHealthBar(void)
barstatus = 2;
}
randlen = M_RandomKey(bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT)))+1;
randtemp = bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT));
if (randtemp > 0)
randlen = M_RandomKey(randtemp)+1;
randsign = M_RandomChance(FRACUNIT/2);
// Right wing.
@ -2034,7 +2059,9 @@ static void K_drawBossHealthBar(void)
randlen--;
if (!randlen)
{
randlen = M_RandomKey(bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT)))+1;
randtemp = bossinfo.visualbar-(bossinfo.visualdiv/(2*FRACUNIT));
if (randtemp > 0)
randlen = M_RandomKey(randtemp)+1;
if (barstatus > 1)
{
rolrand = M_RandomKey(barstatus)+1;
@ -2265,7 +2292,7 @@ void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, IN
colormap = R_GetTranslationColormap(players[tab[i].num].skin, players[tab[i].num].mo->color, GTC_CACHE);
V_DrawMappedPatch(x, y-4, 0, faceprefix[players[tab[i].num].skin][FACE_RANK], colormap);
/*if (gametype == GT_BATTLE && players[tab[i].num].bumpers > 0) -- not enough space for this
/*if ((gametyperules & GTR_BUMPERS) && players[tab[i].num].bumpers > 0) -- not enough space for this
{
INT32 bumperx = x+19;
V_DrawMappedPatch(bumperx-2, y-4, 0, kp_tinybumper[0], colormap);
@ -2280,7 +2307,7 @@ void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, IN
if (tab[i].num == whiteplayer)
V_DrawScaledPatch(x, y-4, 0, kp_facehighlight[(leveltime / 4) % 8]);
if (gametype == GT_BATTLE && players[tab[i].num].bumpers <= 0)
if ((gametyperules & GTR_BUMPERS) && players[tab[i].num].bumpers <= 0)
V_DrawScaledPatch(x-4, y-7, 0, kp_ranknobumpers);
else
{
@ -2291,7 +2318,7 @@ void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, IN
V_DrawScaledPatch(x-5, y+6, 0, kp_facenum[pos]);
}
if (gametype == GT_RACE)
if ((gametyperules & GTR_CIRCUIT))
{
#define timestring(time) va("%i'%02i\"%02i", G_TicsToMinutes(time, true), G_TicsToSeconds(time), G_TicsToCentiseconds(time))
if (scorelines >= 8)
@ -2300,7 +2327,7 @@ void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, IN
V_DrawRightAlignedThinString(x+rightoffset, y-1, hilicol|V_6WIDTHSPACE, timestring(players[tab[i].num].realtime));
else if (players[tab[i].num].pflags & PF_NOCONTEST)
V_DrawRightAlignedThinString(x+rightoffset, y-1, V_6WIDTHSPACE, "NO CONTEST.");
else if (circuitmap)
else
V_DrawRightAlignedThinString(x+rightoffset, y-1, V_6WIDTHSPACE, va("Lap %d", tab[i].count));
}
else
@ -2309,7 +2336,7 @@ void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, IN
V_DrawRightAlignedString(x+rightoffset, y, hilicol, timestring(players[tab[i].num].realtime));
else if (players[tab[i].num].pflags & PF_NOCONTEST)
V_DrawRightAlignedThinString(x+rightoffset, y-1, 0, "NO CONTEST.");
else if (circuitmap)
else
V_DrawRightAlignedString(x+rightoffset, y, 0, va("Lap %d", tab[i].count));
}
#undef timestring
@ -2326,40 +2353,13 @@ void K_DrawTabRankings(INT32 x, INT32 y, playersort_t *tab, INT32 scorelines, IN
}
}
#define RINGANIM_FLIPFRAME (RINGANIM_NUMFRAMES/2)
static void K_drawKartLapsAndRings(void)
static void K_drawKartLaps(void)
{
const boolean uselives = G_GametypeUsesLives();
SINT8 ringanim_realframe = stplyr->karthud[khud_ringframe];
INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN;
UINT8 rn[2];
INT32 ringflip = 0;
UINT8 *ringmap = NULL;
boolean colorring = false;
INT32 ringx = 0;
rn[0] = ((abs(stplyr->rings) / 10) % 10);
rn[1] = (abs(stplyr->rings) % 10);
if (stplyr->rings <= 0 && (leveltime/5 & 1)) // In debt
{
ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE);
colorring = true;
}
else if (stplyr->rings >= 20) // Maxed out
ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_YELLOW, GTC_CACHE);
if (stplyr->karthud[khud_ringframe] > RINGANIM_FLIPFRAME)
{
ringflip = V_FLIP;
ringanim_realframe = RINGANIM_NUMFRAMES-stplyr->karthud[khud_ringframe];
ringx += SHORT((r_splitscreen > 1) ? kp_smallring[ringanim_realframe]->width : kp_ring[ringanim_realframe]->width);
}
if (r_splitscreen > 1)
{
INT32 fx = 0, fy = 0, fr = 0;
INT32 fx = 0, fy = 0;
INT32 flipflag = 0;
// pain and suffering defined below
@ -2385,8 +2385,6 @@ static void K_drawKartLapsAndRings(void)
}
}
fr = fx;
// Laps
V_DrawScaledPatch(fx-2 + (flipflag ? (SHORT(kp_ringstickersplit[1]->width) - 3) : 0), fy, V_HUDTRANS|V_SLIDEIN|splitflags|flipflag, kp_ringstickersplit[0]);
@ -2413,6 +2411,75 @@ static void K_drawKartLapsAndRings(void)
V_DrawScaledPatch(fx+13, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[(stplyr->laps) % 10]);
V_DrawScaledPatch(fx+27, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[(numlaps) % 10]);
}
}
else
{
// Laps
V_DrawScaledPatch(LAPS_X, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_lapsticker);
V_DrawKartString(LAPS_X+33, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", min(stplyr->laps, numlaps), numlaps));
}
}
#define RINGANIM_FLIPFRAME (RINGANIM_NUMFRAMES/2)
static void K_drawRingCounter(void)
{
const boolean uselives = G_GametypeUsesLives();
SINT8 ringanim_realframe = stplyr->karthud[khud_ringframe];
INT32 splitflags = V_SNAPTOBOTTOM|V_SNAPTOLEFT|V_SPLITSCREEN;
UINT8 rn[2];
INT32 ringflip = 0;
UINT8 *ringmap = NULL;
boolean colorring = false;
INT32 ringx = 0, fy = 0;
rn[0] = ((abs(stplyr->rings) / 10) % 10);
rn[1] = (abs(stplyr->rings) % 10);
if (stplyr->rings <= 0 && (leveltime/5 & 1)) // In debt
{
ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_CRIMSON, GTC_CACHE);
colorring = true;
}
else if (stplyr->rings >= 20) // Maxed out
ringmap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_YELLOW, GTC_CACHE);
if (stplyr->karthud[khud_ringframe] > RINGANIM_FLIPFRAME)
{
ringflip = V_FLIP;
ringanim_realframe = RINGANIM_NUMFRAMES-stplyr->karthud[khud_ringframe];
ringx += SHORT((r_splitscreen > 1) ? kp_smallring[ringanim_realframe]->width : kp_ring[ringanim_realframe]->width);
}
if (r_splitscreen > 1)
{
INT32 fx = 0, fr = 0;
INT32 flipflag = 0;
// pain and suffering defined below
if (r_splitscreen < 2) // don't change shit for THIS splitscreen.
{
fx = LAPS_X;
fy = LAPS_Y;
}
else
{
if (stplyr == &players[displayplayers[0]] || stplyr == &players[displayplayers[2]]) // If we are P1 or P3...
{
fx = LAPS_X;
fy = LAPS_Y;
splitflags = V_SNAPTOLEFT|V_SNAPTOBOTTOM|V_SPLITSCREEN;
}
else // else, that means we're P2 or P4.
{
fx = LAPS2_X;
fy = LAPS2_Y;
splitflags = V_SNAPTORIGHT|V_SNAPTOBOTTOM|V_SPLITSCREEN;
flipflag = V_FLIP; // make the string right aligned and other shit
}
}
fr = fx;
// Rings
if (!uselives)
@ -2447,41 +2514,42 @@ static void K_drawKartLapsAndRings(void)
}
else
{
// Laps
V_DrawScaledPatch(LAPS_X, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_lapsticker);
V_DrawKartString(LAPS_X+33, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", min(stplyr->laps, numlaps), numlaps));
fy = LAPS_Y-11;
if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS)
fy -= 4;
// Rings
if (!uselives)
V_DrawScaledPatch(LAPS_X, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringsticker[1]);
V_DrawScaledPatch(LAPS_X, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringsticker[1]);
else
V_DrawScaledPatch(LAPS_X, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringsticker[0]);
V_DrawScaledPatch(LAPS_X, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringsticker[0]);
V_DrawMappedPatch(LAPS_X+ringx+7, LAPS_Y-16, V_HUDTRANS|V_SLIDEIN|splitflags|ringflip, kp_ring[ringanim_realframe], (colorring ? ringmap : NULL));
V_DrawMappedPatch(LAPS_X+ringx+7, fy-5, V_HUDTRANS|V_SLIDEIN|splitflags|ringflip, kp_ring[ringanim_realframe], (colorring ? ringmap : NULL));
if (stplyr->rings < 0) // Draw the minus for ring debt
{
V_DrawMappedPatch(LAPS_X+23, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringdebtminus, ringmap);
V_DrawMappedPatch(LAPS_X+29, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[0]], ringmap);
V_DrawMappedPatch(LAPS_X+35, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[1]], ringmap);
V_DrawMappedPatch(LAPS_X+23, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringdebtminus, ringmap);
V_DrawMappedPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[0]], ringmap);
V_DrawMappedPatch(LAPS_X+35, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[1]], ringmap);
}
else
{
V_DrawMappedPatch(LAPS_X+23, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[0]], ringmap);
V_DrawMappedPatch(LAPS_X+29, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[1]], ringmap);
V_DrawMappedPatch(LAPS_X+23, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[0]], ringmap);
V_DrawMappedPatch(LAPS_X+29, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[rn[1]], ringmap);
}
// SPB ring lock
if (stplyr->pflags & PF_RINGLOCK)
V_DrawScaledPatch(LAPS_X-5, LAPS_Y-28, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringspblock[stplyr->karthud[khud_ringspblock]]);
V_DrawScaledPatch(LAPS_X-5, fy-17, V_HUDTRANS|V_SLIDEIN|splitflags, kp_ringspblock[stplyr->karthud[khud_ringspblock]]);
// Lives
if (uselives)
{
UINT8 *colormap = R_GetTranslationColormap(stplyr->skin, stplyr->skincolor, GTC_CACHE);
V_DrawMappedPatch(LAPS_X+46, LAPS_Y-16, V_HUDTRANS|V_SLIDEIN|splitflags, faceprefix[stplyr->skin][FACE_RANK], colormap);
V_DrawMappedPatch(LAPS_X+46, fy-5, V_HUDTRANS|V_SLIDEIN|splitflags, faceprefix[stplyr->skin][FACE_RANK], colormap);
if (stplyr->lives >= 0)
V_DrawScaledPatch(LAPS_X+63, LAPS_Y-11, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[(stplyr->lives % 10)]); // make sure this doesn't overflow OR underflow
V_DrawScaledPatch(LAPS_X+63, fy, V_HUDTRANS|V_SLIDEIN|splitflags, kp_facenum[(stplyr->lives % 10)]); // make sure this doesn't overflow OR underflow
}
}
}
@ -2498,7 +2566,7 @@ static void K_drawKartAccessibilityIcons(INT32 fx)
if (r_splitscreen < 2) // adjust to speedometer height
{
if (gametype == GT_BATTLE)
if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS)
fy -= 4;
}
else
@ -2588,7 +2656,7 @@ static void K_drawKartSpeedometer(void)
numbers[1] = ((convSpeed / 10) % 10);
numbers[2] = (convSpeed % 10);
if (gametype == GT_BATTLE)
if ((gametyperules & (GTR_BUMPERS|GTR_CIRCUIT)) == GTR_BUMPERS)
battleoffset = -4;
V_DrawScaledPatch(LAPS_X, LAPS_Y-25 + battleoffset, V_HUDTRANS|V_SLIDEIN|splitflags, kp_speedometersticker);
@ -2793,14 +2861,15 @@ static void K_drawKartBumpersOrKarma(void)
else
V_DrawMappedPatch(LAPS_X, LAPS_Y, V_HUDTRANS|V_SLIDEIN|splitflags, kp_bumpersticker, colormap);
if (bossinfo.boss)
V_DrawKartString(LAPS_X+47, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", stplyr->bumpers, maxbumper));
else // TODO BETTER HUD
if (gametyperules & GTR_KARMA) // TODO BETTER HUD
V_DrawKartString(LAPS_X+47, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d %d", stplyr->bumpers, maxbumper, stplyr->overtimekarma / TICRATE));
else
V_DrawKartString(LAPS_X+47, LAPS_Y+3, V_HUDTRANS|V_SLIDEIN|splitflags, va("%d/%d", stplyr->bumpers, maxbumper));
}
}
}
#if 0
static void K_drawKartWanted(void)
{
UINT8 i, numwanted = 0;
@ -2875,6 +2944,7 @@ static void K_drawKartWanted(void)
}
}
}
#endif //if 0
static void K_drawKartPlayerCheck(void)
{
@ -3358,7 +3428,7 @@ static void K_drawKartNameTags(void)
c.z = viewz;
// Maybe shouldn't be handling this here... but the camera info is too good.
if (bossinfo.boss)
if (bossinfo.valid == true)
{
weakspotdraw_t weakspotdraw[NUMWEAKSPOTS];
UINT8 numdraw = 0;
@ -3810,7 +3880,7 @@ static void K_drawKartMinimap(void)
y -= SHORT(AutomapPic->topoffset);
// Draw the super item in Battle
if (gametype == GT_BATTLE && battleovertime.enabled)
if ((gametyperules & GTR_OVERTIME) && battleovertime.enabled)
{
if (battleovertime.enabled >= 10*TICRATE || (battleovertime.enabled & 1))
{
@ -3920,8 +3990,8 @@ static void K_drawKartMinimap(void)
K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, faceprefix[skin][FACE_MINIMAP], colormap, AutomapPic);
// Target reticule
if ((gametype == GT_RACE && players[i].position == spbplace)
|| (gametype == GT_BATTLE && K_IsPlayerWanted(&players[i])))
if (((gametyperules & GTR_CIRCUIT) && players[i].position == spbplace)
|| ((gametyperules & (GTR_BOSS|GTR_POINTLIMIT)) == GTR_POINTLIMIT && K_IsPlayerWanted(&players[i])))
{
K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, NULL, AutomapPic);
}
@ -3979,7 +4049,7 @@ static void K_drawKartMinimap(void)
}
// ...but first, any boss targets.
if (bossinfo.boss)
if (bossinfo.valid == true)
{
for (i = 0; i < NUMWEAKSPOTS; i++)
{
@ -4047,8 +4117,8 @@ static void K_drawKartMinimap(void)
K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, workingPic, colormap, AutomapPic);
// Target reticule
if ((gametype == GT_RACE && players[localplayers[i]].position == spbplace)
|| (gametype == GT_BATTLE && K_IsPlayerWanted(&players[localplayers[i]])))
if (((gametyperules & GTR_CIRCUIT) && players[localplayers[i]].position == spbplace)
|| ((gametyperules & (GTR_BOSS|GTR_POINTLIMIT)) == GTR_POINTLIMIT && K_IsPlayerWanted(&players[localplayers[i]])))
{
K_drawKartMinimapIcon(interpx, interpy, x, y, splitflags, kp_wantedreticle, NULL, AutomapPic);
}
@ -4836,7 +4906,7 @@ void K_drawKartFreePlay(void)
if (!LUA_HudEnabled(hud_freeplay))
return;
if (modeattacking || grandprixinfo.gp || bossinfo.boss || stplyr->spectator)
if (modeattacking || grandprixinfo.gp || bossinfo.valid || stplyr->spectator)
return;
if (lt_exitticker < TICRATE/2)
@ -4883,39 +4953,6 @@ K_drawMiniPing (void)
static void K_drawDistributionDebugger(void)
{
patch_t *patches[NUMKARTRESULTS] = {
kp_sadface[1],
kp_sneaker[1],
kp_rocketsneaker[1],
kp_invincibility[7],
kp_banana[1],
kp_eggman[1],
kp_orbinaut[4],
kp_jawz[1],
kp_mine[1],
kp_landmine[1],
kp_ballhog[1],
kp_selfpropelledbomb[1],
kp_grow[1],
kp_shrink[1],
kp_lightningshield[1],
kp_bubbleshield[1],
kp_flameshield[1],
kp_hyudoro[1],
kp_pogospring[1],
kp_superring[1],
kp_kitchensink[1],
kp_droptarget[1],
kp_gardentop[1],
kp_sneaker[1],
kp_sneaker[1],
kp_banana[1],
kp_orbinaut[4],
kp_orbinaut[4],
kp_jawz[1]
};
itemroulette_t rouletteData = {0};
const fixed_t scale = (FRACUNIT >> 1);
@ -4944,7 +4981,8 @@ static void K_drawDistributionDebugger(void)
y = -pad;
}
V_DrawFixedPatch(x, y, scale, V_SNAPTOTOP, patches[item], NULL);
V_DrawFixedPatch(x, y, scale, V_SNAPTOTOP,
K_GetSmallStaticCachedItemPatch(item), NULL);
// Display amount for multi-items
amount = K_ItemResultToAmount(item);
@ -5024,10 +5062,10 @@ void K_drawKartHUD(void)
return;
}
battlefullscreen = ((gametyperules & (GTR_BUMPERS|GTR_KARMA)) == (GTR_BUMPERS|GTR_KARMA)
battlefullscreen = (!(gametyperules & GTR_CIRCUIT)
&& (stplyr->exiting
|| (stplyr->bumpers <= 0
&& stplyr->karmadelay > 0
|| ((gametyperules & GTR_BUMPERS) && (stplyr->bumpers <= 0)
&& ((gametyperules & GTR_KARMA) && (stplyr->karmadelay > 0))
&& !(stplyr->pflags & PF_ELIMINATED)
&& stplyr->playerstate == PST_LIVE)));
@ -5043,11 +5081,13 @@ void K_drawKartHUD(void)
K_drawKartNameTags();
// Draw WANTED status
#if 0
if (gametype == GT_BATTLE)
{
if (LUA_HudEnabled(hud_wanted))
K_drawKartWanted();
}
#endif
if (LUA_HudEnabled(hud_minimap))
K_drawKartMinimap();
@ -5094,32 +5134,26 @@ void K_drawKartHUD(void)
{
if (LUA_HudEnabled(hud_position))
{
if (bossinfo.boss)
if (bossinfo.valid)
{
K_drawBossHealthBar();
}
else if (gametype == GT_RACE) // Race-only elements (not currently gametyperuleable)
else if (freecam)
;
else if ((gametyperules & GTR_POWERSTONES))
{
if (!islonesome)
{
// Draw the numerical position
K_DrawKartPositionNum(stplyr->position);
}
}
else if (gametype == GT_BATTLE) // Battle-only (ditto)
{
if (!freecam && !battlecapsules)
{
if (!battlecapsules)
K_drawKartEmeralds();
}
}
else if (!islonesome)
K_DrawKartPositionNum(stplyr->position);
}
if (LUA_HudEnabled(hud_gametypeinfo))
{
if (gametyperules & GTR_CIRCUIT)
{
K_drawKartLapsAndRings();
K_drawKartLaps();
}
else if (gametyperules & GTR_BUMPERS)
{
@ -5141,8 +5175,12 @@ void K_drawKartHUD(void)
{
K_drawBlueSphereMeter();
}
else
{
K_drawRingCounter();
}
if (modeattacking && !bossinfo.boss)
if (modeattacking && !bossinfo.valid)
{
// Draw the input UI
if (LUA_HudEnabled(hud_position))
@ -5172,10 +5210,12 @@ void K_drawKartHUD(void)
}
// Race overlays
if (gametype == GT_RACE && !freecam)
if (!freecam)
{
if (stplyr->exiting)
K_drawKartFinish(true);
else if (!(gametyperules & GTR_CIRCUIT))
;
else if (stplyr->karthud[khud_lapanimation] && !r_splitscreen)
K_drawLapStartAnim();
}
@ -5187,7 +5227,7 @@ void K_drawKartHUD(void)
if (modeattacking || freecam) // everything after here is MP and debug only
return;
if (gametype == GT_BATTLE && !r_splitscreen && (stplyr->karthud[khud_yougotem] % 2)) // * YOU GOT EM *
if ((gametyperules & GTR_KARMA) && !r_splitscreen && (stplyr->karthud[khud_yougotem] % 2)) // * YOU GOT EM *
V_DrawScaledPatch(BASEVIDWIDTH/2 - (SHORT(kp_yougotem->width)/2), 32, V_HUDTRANS, kp_yougotem);
// Draw FREE PLAY.

View file

@ -6,7 +6,6 @@
#include "k_kart.h"
#include "k_battle.h"
#include "k_boss.h"
#include "k_pwrlv.h"
#include "k_color.h"
#include "k_respawn.h"
@ -41,6 +40,7 @@
#include "k_follower.h"
#include "k_objects.h"
#include "k_grandprix.h"
#include "k_boss.h"
#include "k_specialstage.h"
#include "k_roulette.h"
@ -109,11 +109,43 @@ void K_TimerInit(void)
boolean domodeattack = ((modeattacking != ATTACKING_NONE)
|| (grandprixinfo.gp == true && grandprixinfo.eventmode != GPEVENT_NONE));
if (specialStage.active == true)
// Rooooooolllling staaaaaaart
if ((gametyperules & (GTR_ROLLINGSTART|GTR_CIRCUIT)) == (GTR_ROLLINGSTART|GTR_CIRCUIT))
{
S_StartSound(NULL, sfx_s25f);
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *player = NULL;
if (playeringame[i] == false)
{
continue;
}
player = &players[i];
if (player->spectator == true)
{
continue;
}
if (player->mo == NULL || P_MobjWasRemoved(player->mo) == true)
{
continue;
}
// Rolling start? lol
P_InstaThrust(player->mo, player->mo->angle, K_GetKartSpeed(player, false, false));
}
}
if ((gametyperules & (GTR_CATCHER|GTR_CIRCUIT)) == (GTR_CATCHER|GTR_CIRCUIT))
{
K_InitSpecialStage();
}
else if (bossinfo.boss == false)
else if (K_CheckBossIntro() == true)
;
else
{
if (!domodeattack)
{
@ -152,7 +184,7 @@ void K_TimerInit(void)
K_BattleInit(domodeattack);
if ((gametyperules & GTR_TIMELIMIT) && !bossinfo.boss && !modeattacking)
if ((gametyperules & GTR_TIMELIMIT) && !modeattacking)
{
if (!K_CanChangeRules(true))
{
@ -162,7 +194,7 @@ void K_TimerInit(void)
}
else
{
timelimitintics = timelimits[gametype] * (60*TICRATE);
timelimitintics = gametypes[gametype]->timelimit * (60*TICRATE);
}
}
else
@ -310,8 +342,6 @@ void K_RegisterKartStuff(void)
CV_RegisterVar(&cv_kartbumpers);
CV_RegisterVar(&cv_kartfrantic);
CV_RegisterVar(&cv_kartencore);
CV_RegisterVar(&cv_kartvoterulechanges);
CV_RegisterVar(&cv_kartgametypepreference);
CV_RegisterVar(&cv_kartspeedometer);
CV_RegisterVar(&cv_kartvoices);
CV_RegisterVar(&cv_kartbot);
@ -342,15 +372,21 @@ boolean K_IsPlayerLosing(player_t *player)
INT32 winningpos = 1;
UINT8 i, pcount = 0;
if (player->pflags & PF_NOCONTEST)
return true;
if (battlecapsules && numtargets == 0)
return true; // Didn't even TRY?
if (battlecapsules || bossinfo.boss)
if (battlecapsules || (gametyperules & GTR_BOSS))
return (player->bumpers <= 0); // anything short of DNF is COOL
if (player->position == 1)
return false;
if (specialstageinfo.valid == true)
return false; // anything short of DNF is COOL
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i] || players[i].spectator)
@ -515,7 +551,7 @@ boolean K_TimeAttackRules(void)
UINT8 playing = 0;
UINT8 i;
if (specialStage.active == true)
if ((gametyperules & (GTR_CATCHER|GTR_CIRCUIT)) == (GTR_CATCHER|GTR_CIRCUIT))
{
// Kind of a hack -- Special Stages
// are expected to be 1-player, so
@ -588,13 +624,22 @@ static fixed_t K_PlayerWeight(mobj_t *mobj, mobj_t *against)
// from causing super crazy bumps.
fixed_t spd = K_GetKartSpeed(mobj->player, false, true);
fixed_t speedfactor = 8 * mapobjectscale;
weight = (mobj->player->kartweight) * FRACUNIT;
if (mobj->player->speed > spd)
weight += FixedDiv((mobj->player->speed - spd), 8 * mapobjectscale);
if (against && against->type == MT_MONITOR)
{
speedfactor /= 5; // speed matters more
}
else
{
if (mobj->player->itemtype == KITEM_BUBBLESHIELD)
weight += 9*FRACUNIT;
}
if (mobj->player->itemtype == KITEM_BUBBLESHIELD)
weight += 9*FRACUNIT;
if (mobj->player->speed > spd)
weight += FixedDiv((mobj->player->speed - spd), speedfactor);
}
return weight;
@ -1272,10 +1317,9 @@ static boolean K_TryDraft(player_t *player, mobj_t *dest, fixed_t minDist, fixed
player->draftpower += add;
}
if (gametype == GT_BATTLE)
if (gametyperules & GTR_CLOSERPLAYERS)
{
// TODO: gametyperules
// Double speed in Battle
// Double speed in smaller environments
player->draftpower += add;
}
}
@ -1304,8 +1348,7 @@ static boolean K_TryDraft(player_t *player, mobj_t *dest, fixed_t minDist, fixed
*/
static void K_UpdateDraft(player_t *player)
{
const boolean addUfo = ((specialStage.active == true)
&& (specialStage.ufo != NULL && P_MobjWasRemoved(specialStage.ufo) == false));
mobj_t *addUfo = K_GetPossibleSpecialTarget();
fixed_t topspd = K_GetKartSpeed(player, false, false);
fixed_t draftdistance;
@ -1335,9 +1378,8 @@ static void K_UpdateDraft(player_t *player)
minDist = 640 * player->mo->scale;
if (gametype == GT_BATTLE)
if (gametyperules & GTR_CLOSERPLAYERS)
{
// TODO: gametyperules
minDist /= 4;
draftdistance *= 2;
leniency *= 4;
@ -1346,10 +1388,10 @@ static void K_UpdateDraft(player_t *player)
// Not enough speed to draft.
if (player->speed >= 20 * player->mo->scale)
{
if (addUfo == true)
if (addUfo != NULL)
{
// Tether off of the UFO!
if (K_TryDraft(player, specialStage.ufo, minDist, draftdistance, leniency) == true)
if (K_TryDraft(player, addUfo, minDist, draftdistance, leniency) == true)
{
return; // Finished doing our draft.
}
@ -1403,11 +1445,11 @@ static void K_UpdateDraft(player_t *player)
fixed_t dist = P_AproxDistance(P_AproxDistance(victim->mo->x - player->mo->x, victim->mo->y - player->mo->y), victim->mo->z - player->mo->z);
K_DrawDraftCombiring(player, victim->mo, dist, draftdistance, true);
}
else if (addUfo == true)
else if (addUfo != NULL)
{
// kind of a hack to not have to mess with how lastdraft works
fixed_t dist = P_AproxDistance(P_AproxDistance(specialStage.ufo->x - player->mo->x, specialStage.ufo->y - player->mo->y), specialStage.ufo->z - player->mo->z);
K_DrawDraftCombiring(player, specialStage.ufo, dist, draftdistance, true);
fixed_t dist = P_AproxDistance(P_AproxDistance(addUfo->x - player->mo->x, addUfo->y - player->mo->y), addUfo->z - player->mo->z);
K_DrawDraftCombiring(player, addUfo, dist, draftdistance, true);
}
}
else // Remove draft speed boost.
@ -2454,7 +2496,7 @@ void K_PlayOvertakeSound(mobj_t *source)
{
boolean tasteful = (!source->player || !source->player->karthud[khud_voices]);
if (!gametype == GT_RACE) // Only in race
if (!(gametyperules & GTR_CIRCUIT)) // Only in race
return;
// 4 seconds from before race begins, 10 seconds afterwards
@ -2963,8 +3005,7 @@ fixed_t K_GetSpindashChargeSpeed(player_t *player)
// (can be higher than this value when overcharged)
const fixed_t val = (10*FRACUNIT/277) + (((player->kartspeed + player->kartweight) + 2) * FRACUNIT) / 45;
// TODO: gametyperules
return (gametype == GT_BATTLE) ? (4 * val) : val;
return (gametyperules & GTR_CLOSERPLAYERS) ? (4 * val) : val;
}
// sets boostpower, speedboost, accelboost, and handleboost to whatever we need it to be
@ -3099,9 +3140,8 @@ static void K_GetKartBoostPower(player_t *player)
// 30% - 44%, each point of speed adds 1.75%
fixed_t draftspeed = ((3*FRACUNIT)/10) + ((player->kartspeed-1) * ((7*FRACUNIT)/400));
if (gametype == GT_BATTLE)
if (gametyperules & GTR_CLOSERPLAYERS)
{
// TODO: gametyperules
draftspeed *= 2;
}
@ -3224,7 +3264,7 @@ fixed_t K_GetKartAccel(player_t *player)
k_accel += 17 * (9 - player->kartspeed); // 121 - 257
// karma bomb gets 2x acceleration
if (gametype == GT_BATTLE && player->bumpers <= 0)
if ((gametyperules & GTR_BUMPERS) && player->bumpers <= 0)
k_accel *= 2;
// Marble Garden Top gets 1200% accel
@ -3238,9 +3278,8 @@ UINT16 K_GetKartFlashing(player_t *player)
{
UINT16 tics = flashingtics;
if (gametype == GT_BATTLE)
if (gametyperules & GTR_BUMPERS)
{
// TODO: gametyperules
return 1;
}
@ -3303,7 +3342,8 @@ SINT8 K_GetForwardMove(player_t *player)
return 0;
}
if (player->sneakertimer || player->spindashboost)
if (player->sneakertimer || player->spindashboost
|| (((gametyperules & (GTR_ROLLINGSTART|GTR_CIRCUIT)) == (GTR_ROLLINGSTART|GTR_CIRCUIT)) && (leveltime < TICRATE/2)))
{
return MAXPLMOVE;
}
@ -3545,6 +3585,12 @@ void K_BattleAwardHit(player_t *player, player_t *victim, mobj_t *inflictor, UIN
UINT8 points = 1;
boolean trapItem = false;
if (!(gametyperules & GTR_POINTLIMIT))
{
// No points in this gametype.
return;
}
if (player == NULL || victim == NULL)
{
// Invalid player or victim
@ -3580,11 +3626,8 @@ void K_BattleAwardHit(player_t *player, player_t *victim, mobj_t *inflictor, UIN
}
}
if (gametyperules & GTR_POINTLIMIT)
{
P_AddPlayerScore(player, points);
K_SpawnBattlePoints(player, victim, points);
}
P_AddPlayerScore(player, points);
K_SpawnBattlePoints(player, victim, points);
}
void K_SpinPlayer(player_t *player, mobj_t *inflictor, mobj_t *source, INT32 type)
@ -4132,7 +4175,7 @@ void K_HandleBumperChanges(player_t *player, UINT8 prevBumpers)
player->karmadelay = comebacktime;
if (bossinfo.boss)
if (gametyperules & GTR_BOSS)
{
P_DoTimeOver(player);
}
@ -5056,7 +5099,7 @@ void K_SpawnDraftDust(mobj_t *mo)
{
UINT8 leniency = (3*TICRATE)/4 + ((mo->player->kartweight-1) * (TICRATE/4));
if (gametype == GT_BATTLE)
if (gametyperules & GTR_CLOSERPLAYERS)
leniency *= 4;
ang = mo->player->drawangle;
@ -6183,17 +6226,47 @@ void K_DropHnextList(player_t *player, boolean keepshields)
}
}
SINT8 K_GetTotallyRandomResult(UINT8 useodds)
{
INT32 spawnchance[NUMKARTRESULTS];
INT32 totalspawnchance = 0;
INT32 i;
memset(spawnchance, 0, sizeof (spawnchance));
for (i = 1; i < NUMKARTRESULTS; i++)
{
// Avoid calling K_FillItemRouletteData since that
// function resets PR_ITEM_ROULETTE.
spawnchance[i] = (
totalspawnchance += K_KartGetItemOdds(NULL, NULL, useodds, i)
);
}
if (totalspawnchance > 0)
{
totalspawnchance = P_RandomKey(PR_ITEM_ROULETTE, totalspawnchance);
for (i = 0; i < NUMKARTRESULTS && spawnchance[i] <= totalspawnchance; i++);
}
else
{
i = KITEM_SAD;
}
return i;
}
mobj_t *K_CreatePaperItem(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 flip, UINT8 type, UINT8 amount)
{
mobj_t *drop = P_SpawnMobj(x, y, z, MT_FLOATINGITEM);
mobj_t *backdrop = P_SpawnMobjFromMobj(drop, 0, 0, 0, MT_OVERLAY);
P_SetTarget(&backdrop->target, drop);
P_SetMobjState(backdrop, S_ITEMBACKDROP);
P_SetScale(drop, drop->scale>>4);
drop->destscale = (3*drop->destscale)/2;
drop->angle = angle;
P_Thrust(drop,
FixedAngle(P_RandomFixed(PR_ITEM_ROULETTE) * 180) + angle,
@ -6205,62 +6278,31 @@ mobj_t *K_CreatePaperItem(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8
if (type == 0)
{
itemroulette_t rouletteData = {0};
UINT8 useodds = 0;
INT32 spawnchance[NUMKARTRESULTS];
INT32 totalspawnchance = 0;
INT32 i;
const SINT8 i = K_GetTotallyRandomResult(amount);
memset(spawnchance, 0, sizeof (spawnchance));
// TODO: this is bad!
// K_KartGetItemResult requires a player
// but item roulette will need rewritten to change this
useodds = amount;
const SINT8 newType = K_ItemResultToType(i);
const UINT8 newAmount = K_ItemResultToAmount(i);
K_FillItemRouletteData(NULL, &rouletteData);
for (i = 1; i < NUMKARTRESULTS; i++)
if (newAmount > 1)
{
spawnchance[i] = (
totalspawnchance += K_KartGetItemOdds(NULL, &rouletteData, useodds, i)
);
}
UINT8 j;
if (totalspawnchance > 0)
{
UINT8 newType;
UINT8 newAmount;
totalspawnchance = P_RandomKey(PR_ITEM_ROULETTE, totalspawnchance);
for (i = 0; i < NUMKARTRESULTS && spawnchance[i] <= totalspawnchance; i++);
// TODO: this is bad!
// K_KartGetItemResult requires a player
// but item roulette will need rewritten to change this
newType = K_ItemResultToType(i);
newAmount = K_ItemResultToAmount(i);
if (newAmount > 1)
for (j = 0; j < newAmount-1; j++)
{
UINT8 j;
for (j = 0; j < newAmount-1; j++)
{
K_CreatePaperItem(
x, y, z,
angle, flip,
newType, 1
);
}
K_CreatePaperItem(
x, y, z,
angle, flip,
newType, 1
);
}
}
drop->threshold = newType;
drop->movecount = 1;
}
else
{
drop->threshold = 1;
drop->movecount = 1;
}
drop->threshold = newType;
drop->movecount = 1;
}
else
{
@ -6273,6 +6315,11 @@ mobj_t *K_CreatePaperItem(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8
P_SetTarget(&backdrop->tracer, drop);
backdrop->flags2 |= MF2_LINKDRAW;
if (gametyperules & GTR_BUMPERS)
{
drop->fuse = BATTLE_DESPAWN_TIME;
}
return drop;
}
@ -6793,10 +6840,10 @@ mobj_t *K_FindJawzTarget(mobj_t *actor, player_t *source, angle_t range)
mobj_t *wtarg = NULL;
INT32 i;
if (specialStage.active == true)
if (specialstageinfo.valid == true)
{
// Always target the UFO.
return specialStage.ufo;
// Always target the UFO (but not the emerald)
return K_GetPossibleSpecialTarget();
}
for (i = 0; i < MAXPLAYERS; i++)
@ -7472,7 +7519,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
K_SpawnGrowShrinkParticles(player->mo, player->growshrinktimer);
}
if (gametype == GT_RACE && player->rings <= 0) // spawn ring debt indicator
if (!(gametyperules & GTR_SPHERES) && player->rings <= 0) // spawn ring debt indicator
{
mobj_t *debtflag = P_SpawnMobj(player->mo->x + player->mo->momx, player->mo->y + player->mo->momy,
player->mo->z + P_GetMobjZMovement(player->mo) + player->mo->height + (24*player->mo->scale), MT_THOK);
@ -7538,8 +7585,6 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
//CONS_Printf("cam: %d, dest: %d\n", player->karthud[khud_boostcam], player->karthud[khud_destboostcam]);
}
player->karthud[khud_timeovercam] = 0;
// Make ABSOLUTELY SURE that your flashing tics don't get set WHILE you're still in hit animations.
if (player->spinouttimer != 0 || player->wipeoutslow != 0)
{
@ -7790,7 +7835,7 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
if (player->eggmanexplode)
{
if (player->spectator || (gametype == GT_BATTLE && !player->bumpers))
if (player->spectator || ((gametyperules & GTR_BUMPERS) && player->bumpers <= 0))
player->eggmanexplode = 0;
else
{
@ -8032,10 +8077,11 @@ void K_KartPlayerAfterThink(player_t *player)
mobj_t *ret = NULL;
if (specialStage.active == true && lastTargID == MAXPLAYERS)
if (specialstageinfo.valid == true
&& lastTargID == MAXPLAYERS)
{
// Aiming at the UFO.
lastTarg = specialStage.ufo;
// Aiming at the UFO (but never the emerald).
lastTarg = K_GetPossibleSpecialTarget();
}
else if ((lastTargID >= 0 && lastTargID <= MAXPLAYERS)
&& playeringame[lastTargID] == true)
@ -9332,15 +9378,18 @@ static INT32 K_FlameShieldMax(player_t *player)
UINT8 numplayers = 0;
UINT8 i;
for (i = 0; i < MAXPLAYERS; i++)
if (gametyperules & GTR_CIRCUIT)
{
if (playeringame[i] && !players[i].spectator)
numplayers++;
if (players[i].position == 1)
disttofinish = players[i].distancetofinish;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && !players[i].spectator)
numplayers++;
if (players[i].position == 1)
disttofinish = players[i].distancetofinish;
}
}
if (numplayers <= 1 || gametype == GT_BATTLE)
if (numplayers <= 1)
{
return 16; // max when alone, for testing
// and when in battle, for chaos
@ -9594,8 +9643,7 @@ static void K_KartSpindash(player_t *player)
{
fixed_t thrust = FixedMul(player->mo->scale, player->spindash*FRACUNIT/5);
// TODO: gametyperules
if (gametype == GT_BATTLE)
if (gametyperules & GTR_CLOSERPLAYERS)
thrust *= 2;
// Give a bit of a boost depending on charge.
@ -10402,8 +10450,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
player->mo->destscale = FixedMul(player->mo->destscale, SHRINK_SCALE);
}
// TODO: gametyperules
player->growshrinktimer = max(player->growshrinktimer, (gametype == GT_BATTLE ? 8 : 12) * TICRATE);
player->growshrinktimer = max(player->growshrinktimer, ((gametyperules & GTR_CLOSERPLAYERS) ? 8 : 12) * TICRATE);
if (player->invincibilitytimer > 0)
{
@ -10561,8 +10608,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
if ((cmd->buttons & BT_ATTACK) && (player->pflags & PF_HOLDREADY))
{
// TODO: gametyperules
const INT32 incr = gametype == GT_BATTLE ? 4 : 2;
const INT32 incr = (gametyperules & GTR_CLOSERPLAYERS) ? 4 : 2;
if (player->flamedash == 0)
{
@ -10598,8 +10644,7 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
{
player->pflags |= PF_HOLDREADY;
// TODO: gametyperules
if (gametype != GT_BATTLE || leveltime % 6 == 0)
if (!(gametyperules & GTR_CLOSERPLAYERS) || leveltime % 6 == 0)
{
if (player->flamemeter > 0)
player->flamemeter--;
@ -10721,18 +10766,13 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
if (player->hyudorotimer > 0)
{
INT32 hyu = hyudorotime;
if (gametype == GT_RACE)
hyu *= 2; // double in race
if (leveltime & 1)
{
player->mo->renderflags |= RF_DONTDRAW;
}
else
{
if (player->hyudorotimer >= (TICRATE/2) && player->hyudorotimer <= hyu-(TICRATE/2))
if (player->hyudorotimer >= (TICRATE/2) && player->hyudorotimer <= hyudorotime-(TICRATE/2))
player->mo->renderflags &= ~K_GetPlayerDontDrawFlag(player);
else
player->mo->renderflags &= ~RF_DONTDRAW;
@ -10745,17 +10785,17 @@ void K_MoveKartPlayer(player_t *player, boolean onground)
player->mo->renderflags &= ~RF_DONTDRAW;
}
if (gametype == GT_BATTLE && player->bumpers <= 0) // dead in match? you da bomb
if (!(gametyperules & GTR_BUMPERS) || player->bumpers > 0)
{
player->mo->renderflags &= ~(RF_TRANSMASK|RF_BRIGHTMASK);
}
else // dead in match? you da bomb
{
K_DropItems(player); //K_StripItems(player);
K_StripOther(player);
player->mo->renderflags |= RF_GHOSTLY;
player->flashing = player->karmadelay;
}
else if (gametype == GT_RACE || player->bumpers > 0)
{
player->mo->renderflags &= ~(RF_TRANSMASK|RF_BRIGHTMASK);
}
if (player->trickpanel == 1)
{
@ -10968,7 +11008,7 @@ void K_CheckSpectateStatus(void)
continue;
if (leveltime > (starttime + 20*TICRATE)) // DON'T allow if the match is 20 seconds in
return;
if (gametype == GT_RACE && players[i].laps >= 2) // DON'T allow if the race is at 2 laps
if ((gametyperules & GTR_CIRCUIT) && players[i].laps >= 2) // DON'T allow if the race is at 2 laps
return;
continue;
}
@ -11164,4 +11204,27 @@ void K_HandleDirectionalInfluence(player_t *player)
player->mo->momy = FixedMul(speed, finalY);
}
void K_UpdateMobjItemOverlay(mobj_t *part, SINT8 itemType, UINT8 itemCount)
{
switch (itemType)
{
case KITEM_ORBINAUT:
part->sprite = SPR_ITMO;
part->frame = FF_FULLBRIGHT|FF_PAPERSPRITE|K_GetOrbinautItemFrame(itemCount);
break;
case KITEM_INVINCIBILITY:
part->sprite = SPR_ITMI;
part->frame = FF_FULLBRIGHT|FF_PAPERSPRITE|K_GetInvincibilityItemFrame();
break;
case KITEM_SAD:
part->sprite = SPR_ITEM;
part->frame = FF_FULLBRIGHT|FF_PAPERSPRITE;
break;
default:
part->sprite = SPR_ITEM;
part->frame = FF_FULLBRIGHT|FF_PAPERSPRITE|(itemType);
break;
}
}
//}

View file

@ -138,6 +138,7 @@ INT32 K_GetKartDriftSparkValueForStage(player_t *player, UINT8 stage);
void K_SpawnDriftBoostExplosion(player_t *player, int stage);
void K_SpawnDriftElectricSparks(player_t *player, int color, boolean shockwave);
void K_KartUpdatePosition(player_t *player);
SINT8 K_GetTotallyRandomResult(UINT8 useodds);
mobj_t *K_CreatePaperItem(fixed_t x, fixed_t y, fixed_t z, angle_t angle, SINT8 flip, UINT8 type, UINT8 amount);
void K_DropItems(player_t *player);
void K_DropRocketSneaker(player_t *player);
@ -197,6 +198,8 @@ fixed_t K_ItemScaleForPlayer(player_t *player);
void K_SetItemOut(player_t *player);
void K_UnsetItemOut(player_t *player);
void K_UpdateMobjItemOverlay(mobj_t *part, SINT8 itemType, UINT8 itemCount);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -119,7 +119,11 @@ struct menucolor_t {
extern menucolor_t *menucolorhead, *menucolortail;
extern CV_PossibleValue_t gametype_cons_t[];
extern INT16 menugametype;
void M_NextMenuGametype(UINT32 forbidden);
void M_PrevMenuGametype(UINT32 forbidden);
void M_HandleHostMenuGametype(INT32 choice);
void M_HandlePauseMenuGametype(INT32 choice);
//
// MENU TYPEDEFS
@ -417,6 +421,7 @@ extern menu_t MISC_StatisticsDef;
typedef enum
{
mpause_addons = 0,
mpause_changegametype,
mpause_switchmap,
mpause_restartmap,
mpause_tryagain,
@ -435,9 +440,6 @@ typedef enum
mpause_title,
} mpause_e;
extern menuitem_t PAUSE_GamemodesMenu[];
extern menu_t PAUSE_GamemodesDef;
extern menuitem_t PAUSE_PlaybackMenu[];
extern menu_t PAUSE_PlaybackMenuDef;
@ -696,7 +698,6 @@ extern struct cupgrid_s {
size_t cappages;
tic_t previewanim;
boolean grandprix; // Setup grand prix server after picking
boolean netgame; // Start the game in an actual server
} cupgrid;
typedef struct levelsearch_s {
@ -713,6 +714,7 @@ extern struct levellist_s {
UINT16 dest;
INT16 choosemap;
UINT8 newgametype;
UINT8 guessgt;
levelsearch_t levelsearch;
boolean netgame; // Start the game in an actual server
} levellist;
@ -773,7 +775,6 @@ void M_MPOptSelect(INT32 choice);
void M_MPOptSelectInit(INT32 choice);
void M_MPOptSelectTick(void);
boolean M_MPResetOpts(void);
extern consvar_t cv_dummygametype; // lazy hack to allow us to select the GT on the server host submenu
extern consvar_t cv_dummyip; // I HAVE
// HAVE YOUR IP ADDRESS (This just the hack Cvar we'll type into and then it apends itself to "connect" in the console for IP join)

View file

@ -89,12 +89,15 @@ 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 hedgehog standing in this free-for-all!",
{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},
};
@ -359,8 +362,8 @@ menuitem_t PLAY_MP_Host[] =
{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_CVAR, "Gamemode", "Are we racing? Or perhaps battling?",
NULL, {.cvar = &cv_dummygametype}, 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},
@ -1132,9 +1135,6 @@ menuitem_t OPTIONS_Server[] =
{IT_STRING | IT_CVAR, "Vote Timer", "Set how long players have to vote.",
NULL, {.cvar = &cv_votetime}, 0, 0},
{IT_STRING | IT_CVAR, "Vote Mode Change", "Set how often voting proposes a different gamemode.",
NULL, {.cvar = &cv_kartvoterulechanges}, 0, 0},
{IT_SPACE | IT_NOTHING, NULL, NULL,
NULL, {NULL}, 0, 0},
@ -1593,8 +1593,11 @@ menuitem_t PAUSE_Main[] =
{IT_STRING | IT_CALL, "ADDONS", "M_ICOADD",
NULL, {.routine = M_Addons}, 0, 0},
{IT_STRING | IT_SUBMENU, "CHANGE MAP", "M_ICOMAP",
NULL, {.submenu = &PAUSE_GamemodesDef}, 0, 0},
{IT_STRING | IT_KEYHANDLER, "GAMETYPE", "M_ICOGAM",
NULL, {.routine = M_HandlePauseMenuGametype}, 0, 0},
{IT_STRING | IT_CALL, "CHANGE MAP", "M_ICOMAP",
NULL, {.routine = M_LevelSelectInit}, 0, -1},
{IT_STRING | IT_CALL, "RESTART MAP", "M_ICORE",
NULL, {.routine = M_RestartMap}, 0, 0},
@ -1647,20 +1650,6 @@ menu_t PAUSE_MainDef = {
M_PauseInputs
};
// PAUSE : Map switching gametype selection (In case you want to pick from battle / race...)
menuitem_t PAUSE_GamemodesMenu[] =
{
{IT_STRING | IT_CALL, "Race", "Select which gamemode to choose a new map from.",
NULL, {.routine = M_LevelSelectInit}, 0, GT_RACE},
{IT_STRING | IT_CALL, "Battle", "Select which gamemode to choose a new map from.",
NULL, {.routine = M_LevelSelectInit}, 0, GT_BATTLE},
{IT_STRING | IT_CALL, "Back", NULL, NULL, {.routine = M_GoBack}, 0, 0},
};
menu_t PAUSE_GamemodesDef = KARTGAMEMODEMENU(PAUSE_GamemodesMenu, &PAUSE_MainDef);
// Replay popup menu
menuitem_t PAUSE_PlaybackMenu[] =
{

View file

@ -541,12 +541,7 @@ void M_Drawer(void)
}
else
{
#ifdef DEVELOP // Development -- show revision / branch info
V_DrawThinString(vid.dupx, vid.height - 20*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, compbranch);
V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, comprevision);
#else // Regular build
V_DrawThinString(vid.dupx, vid.height - 10*vid.dupy, V_NOSCALESTART|V_TRANSLUCENT|V_ALLOWLOWERCASE, va("%s", VERSIONSTRING));
#endif
F_VersionDrawer();
}
@ -1931,9 +1926,7 @@ static void M_DrawCupPreview(INT16 y, levelsearch_t *levelsearch)
INT16 map, start = M_GetFirstLevelInList(&i, levelsearch);
UINT8 starti = i;
V_DrawFill(0, y, BASEVIDWIDTH, 54, 31);
if (levelsearch->cup && !M_CupLocked(levelsearch->cup))
if (levelsearch->cup && maxlevels > 0)
{
add = (cupgrid.previewanim / 82) % maxlevels;
map = start;
@ -1979,16 +1972,18 @@ static void M_DrawCupPreview(INT16 y, levelsearch_t *levelsearch)
}
}
static void M_DrawCupTitle(INT16 y, cupheader_t *cup)
static void M_DrawCupTitle(INT16 y, levelsearch_t *levelsearch)
{
UINT8 temp = 0;
V_DrawScaledPatch(0, y, 0, W_CachePatchName("MENUHINT", PU_CACHE));
if (cup)
if (levelsearch->cup)
{
boolean unlocked = !M_CupLocked(cup);
boolean unlocked = (M_GetFirstLevelInList(&temp, levelsearch) != NEXTMAP_INVALID);
UINT8 *colormap = R_GetTranslationColormap(TC_RAINBOW, SKINCOLOR_GREY, GTC_MENUCACHE);
patch_t *icon = W_CachePatchName(cup->icon, PU_CACHE);
const char *str = (unlocked ? va("%s Cup", cup->name) : "???");
patch_t *icon = W_CachePatchName(levelsearch->cup->icon, PU_CACHE);
const char *str = (unlocked ? va("%s Cup", levelsearch->cup->name) : "???");
INT16 offset = V_LSTitleLowStringWidth(str, 0) / 2;
V_DrawLSTitleLowString(BASEVIDWIDTH/2 - offset, y+6, 0, str);
@ -2002,32 +1997,35 @@ static void M_DrawCupTitle(INT16 y, cupheader_t *cup)
else
{
if (currentMenu == &PLAY_LevelSelectDef)
V_DrawCenteredLSTitleLowString(BASEVIDWIDTH/2, y+6, 0, va("%s Mode", Gametype_Names[levellist.newgametype]));
{
UINT8 namedgt = (levellist.guessgt != MAXGAMETYPES) ? levellist.guessgt : levellist.newgametype;
V_DrawCenteredLSTitleLowString(BASEVIDWIDTH/2, y+6, 0, va("%s Mode", gametypes[namedgt]->name));
}
}
}
void M_DrawCupSelect(void)
{
UINT8 i, j;
UINT8 i, j, temp = 0;
levelsearch_t templevelsearch = levellist.levelsearch; // full copy
templevelsearch.cup = cupgrid.builtgrid[CUPMENU_CURSORID];
for (i = 0; i < CUPMENU_COLUMNS; i++)
{
for (j = 0; j < CUPMENU_ROWS; j++)
{
size_t id = (i + (j * CUPMENU_COLUMNS)) + (cupgrid.pageno * (CUPMENU_COLUMNS * CUPMENU_ROWS));
cupheader_t *iconcup = cupgrid.builtgrid[id];
patch_t *patch = NULL;
INT16 x, y;
INT16 icony = 7;
if (!iconcup)
if (!cupgrid.builtgrid[id])
break;
/*if (iconcup->emeraldnum == 0)
templevelsearch.cup = cupgrid.builtgrid[id];
/*if (templevelsearch.cup->emeraldnum == 0)
patch = W_CachePatchName("CUPMON3A", PU_CACHE);
else*/ if (iconcup->emeraldnum > 7)
else*/ if (templevelsearch.cup->emeraldnum > 7)
{
patch = W_CachePatchName("CUPMON2A", PU_CACHE);
icony = 5;
@ -2040,14 +2038,14 @@ void M_DrawCupSelect(void)
V_DrawScaledPatch(x, y, 0, patch);
if (M_CupLocked(iconcup))
if (M_GetFirstLevelInList(&temp, &templevelsearch) == NEXTMAP_INVALID)
{
patch_t *st = W_CachePatchName(va("ICONST0%d", (cupgrid.previewanim % 4) + 1), PU_CACHE);
V_DrawScaledPatch(x + 8, y + icony, 0, st);
}
else
{
V_DrawScaledPatch(x + 8, y + icony, 0, W_CachePatchName(iconcup->icon, PU_CACHE));
V_DrawScaledPatch(x + 8, y + icony, 0, W_CachePatchName(templevelsearch.cup->icon, PU_CACHE));
V_DrawScaledPatch(x + 8, y + icony, 0, W_CachePatchName("CUPBOX", PU_CACHE));
}
}
@ -2058,8 +2056,12 @@ void M_DrawCupSelect(void)
0, W_CachePatchName("CUPCURS", PU_CACHE)
);
templevelsearch.cup = cupgrid.builtgrid[CUPMENU_CURSORID];
V_DrawFill(0, 146 + (24*menutransition.tics), BASEVIDWIDTH, 54, 31);
M_DrawCupPreview(146 + (24*menutransition.tics), &templevelsearch);
M_DrawCupTitle(120 - (24*menutransition.tics), templevelsearch.cup);
M_DrawCupTitle(120 - (24*menutransition.tics), &templevelsearch);
}
static void M_DrawHighLowLevelTitle(INT16 x, INT16 y, INT16 map)
@ -2225,7 +2227,7 @@ void M_DrawLevelSelect(void)
map = M_GetNextLevelInList(map, &j, &levellist.levelsearch);
}
M_DrawCupTitle(tay, levellist.levelsearch.cup);
M_DrawCupTitle(tay, &levellist.levelsearch);
}
void M_DrawTimeAttack(void)
@ -2261,7 +2263,8 @@ void M_DrawTimeAttack(void)
laprec = mapheaderinfo[map]->mainrecord->lap;
}
if (levellist.newgametype != GT_BATTLE)
if ((gametypes[levellist.newgametype]->rules & GTR_CIRCUIT)
&& (mapheaderinfo[map]->numlaps != 1))
{
V_DrawRightAlignedString(rightedge-12, timeheight, highlightflags, "BEST LAP:");
K_drawKartTimestamp(laprec, 162+t, timeheight+6, 0, 2);
@ -2462,6 +2465,20 @@ void M_DrawMPHost(void)
}
break;
}
case IT_KEYHANDLER:
{
if (currentMenu->menuitems[i].itemaction.routine != M_HandleHostMenuGametype)
break;
w = V_ThinStringWidth(gametypes[menugametype]->name, V_6WIDTHSPACE);
V_DrawThinString(xp + 138 - w, yp, highlightflags|V_6WIDTHSPACE, gametypes[menugametype]->name);
if (i == itemOn)
{
V_DrawCharacter(xp + 138 - 10 - w - (skullAnimCounter/5), yp, '\x1C' | highlightflags, false); // left arrow
V_DrawCharacter(xp + 138 + 2 + (skullAnimCounter/5), yp, '\x1D' | highlightflags, false); // right arrow
}
break;
}
}
xp += 5;
@ -3680,27 +3697,19 @@ void M_DrawPause(void)
case IT_STRING:
{
patch_t *pp;
UINT8 *colormap = NULL;
if (i == itemOn)
if (i == itemOn && (i == mpause_restartmap || i == mpause_tryagain))
{
if (i == mpause_restartmap || i == mpause_tryagain)
{
pp = W_CachePatchName(
va("M_ICOR2%c", ('A'+(pausemenu.ticker & 1))),
PU_CACHE);
}
else
{
char iconame[9]; // 8 chars + \0
strcpy(iconame, currentMenu->menuitems[i].tooltip);
iconame[7] = '2'; // Yes this is a stupid hack. Replace the last character with a 2 when we're selecting this graphic.
pp = W_CachePatchName(iconame, PU_CACHE);
}
pp = W_CachePatchName(
va("M_ICOR2%c", ('A'+(pausemenu.ticker & 1))),
PU_CACHE);
}
else
{
pp = W_CachePatchName(currentMenu->menuitems[i].tooltip, PU_CACHE);
if (i == itemOn)
colormap = yellowmap;
}
// 294 - 261 = 33
@ -3711,7 +3720,7 @@ void M_DrawPause(void)
// This double ternary is awful, yes.
dypos = ypos + pausemenu.offset;
V_DrawFixedPatch( ((i == itemOn ? (294 - pausemenu.offset*2/3 * (dypos > 100 ? 1 : -1)) : 261) + offset) << FRACBITS, (dypos)*FRACUNIT, FRACUNIT, 0, pp, NULL);
V_DrawFixedPatch( ((i == itemOn ? (294 - pausemenu.offset*2/3 * (dypos > 100 ? 1 : -1)) : 261) + offset) << FRACBITS, (dypos)*FRACUNIT, FRACUNIT, 0, pp, colormap);
ypos += 50;
itemsdrawn++; // We drew that!
@ -3760,12 +3769,26 @@ void M_DrawPause(void)
word1[word1len] = '\0';
word2[word2len] = '\0';
// If there's no 2nd word, take this opportunity to center this line of text.
if (word1len)
V_DrawCenteredLSTitleHighString(220 + offset*2, 75 + (!word2len ? 10 : 0), 0, word1);
if (itemOn == mpause_changegametype)
{
INT32 w = V_LSTitleLowStringWidth(gametypes[menugametype]->name, 0)/2;
if (word2len)
V_DrawCenteredLSTitleLowString(220 + offset*2, 103, 0, word2);
if (word1len)
V_DrawCenteredLSTitleHighString(220 + offset*2, 75, 0, word1);
V_DrawLSTitleLowString(220-w + offset*2, 103, V_YELLOWMAP, gametypes[menugametype]->name);
V_DrawCharacter(220-w + offset*2 - 8 - (skullAnimCounter/5), 103+6, '\x1C' | V_YELLOWMAP, false); // left arrow
V_DrawCharacter(220+w + offset*2 + 4 + (skullAnimCounter/5), 103+6, '\x1D' | V_YELLOWMAP, false); // right arrow
}
else
{
// If there's no 2nd word, take this opportunity to center this line of text.
if (word1len)
V_DrawCenteredLSTitleHighString(220 + offset*2, 75 + (!word2len ? 10 : 0), 0, word1);
if (word2len)
V_DrawCenteredLSTitleLowString(220 + offset*2, 103, 0, word2);
}
}
tic_t playback_last_menu_interaction_leveltime = 0;
@ -3964,9 +3987,22 @@ static void M_DrawReplayHutReplayInfo(menudemo_t *demoref)
if (demoref->numlaps)
V_DrawThinString(x, y+9, V_SNAPTOTOP|V_ALLOWLOWERCASE, va("(%d laps)", demoref->numlaps));
V_DrawString(x, y+20, V_SNAPTOTOP|V_ALLOWLOWERCASE, demoref->gametype == GT_RACE ?
va("Race (%s speed)", kartspeed_cons_t[(demoref->kartspeed & ~DF_ENCORE) + 1].strvalue) :
"Battle Mode");
{
const char *gtstring;
if (demoref->gametype < 0)
{
gtstring = "Custom (not loaded)";
}
else
{
gtstring = gametypes[demoref->gametype]->name;
if ((gametypes[demoref->gametype]->rules & GTR_CIRCUIT))
gtstring = va("%s (%s)", gtstring, kartspeed_cons_t[(demoref->kartspeed & ~DF_ENCORE) + 1].strvalue);
}
V_DrawString(x, y+20, V_SNAPTOTOP|V_ALLOWLOWERCASE, gtstring);
}
if (!demoref->standings[0].ranking)
{
@ -3979,30 +4015,33 @@ static void M_DrawReplayHutReplayInfo(menudemo_t *demoref)
V_DrawThinString(x, y+29, V_SNAPTOTOP|highlightflags, "WINNER");
V_DrawString(x+38, y+30, V_SNAPTOTOP|V_ALLOWLOWERCASE, demoref->standings[0].name);
if (demoref->gametype == GT_RACE)
if (demoref->gametype >= 0)
{
V_DrawThinString(x, y+39, V_SNAPTOTOP|highlightflags, "TIME");
}
else
{
V_DrawThinString(x, y+39, V_SNAPTOTOP|highlightflags, "SCORE");
}
if (gametypes[demoref->gametype]->rules & GTR_POINTLIMIT)
{
V_DrawThinString(x, y+39, V_SNAPTOTOP|highlightflags, "SCORE");
}
else
{
V_DrawThinString(x, y+39, V_SNAPTOTOP|highlightflags, "TIME");
}
if (demoref->standings[0].timeorscore == (UINT32_MAX-1))
{
V_DrawThinString(x+32, y+39, V_SNAPTOTOP, "NO CONTEST");
}
else if (demoref->gametype == GT_RACE)
{
V_DrawRightAlignedString(x+84, y+40, V_SNAPTOTOP, va("%d'%02d\"%02d",
G_TicsToMinutes(demoref->standings[0].timeorscore, true),
G_TicsToSeconds(demoref->standings[0].timeorscore),
G_TicsToCentiseconds(demoref->standings[0].timeorscore)
));
}
else
{
V_DrawString(x+32, y+40, V_SNAPTOTOP, va("%d", demoref->standings[0].timeorscore));
if (demoref->standings[0].timeorscore == (UINT32_MAX-1))
{
V_DrawThinString(x+32, y+39, V_SNAPTOTOP, "NO CONTEST");
}
else if (gametypes[demoref->gametype]->rules & GTR_POINTLIMIT)
{
V_DrawString(x+32, y+40, V_SNAPTOTOP, va("%d", demoref->standings[0].timeorscore));
}
else
{
V_DrawRightAlignedString(x+84, y+40, V_SNAPTOTOP, va("%d'%02d\"%02d",
G_TicsToMinutes(demoref->standings[0].timeorscore, true),
G_TicsToSeconds(demoref->standings[0].timeorscore),
G_TicsToCentiseconds(demoref->standings[0].timeorscore)
));
}
}
// Character face!
@ -4197,14 +4236,16 @@ void M_DrawReplayStartMenu(void)
if (demoref->standings[i].timeorscore == UINT32_MAX-1)
V_DrawThinString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, "NO CONTEST");
else if (demoref->gametype == GT_RACE)
else if (demoref->gametype < 0)
;
else if (gametypes[demoref->gametype]->rules & GTR_POINTLIMIT)
V_DrawString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d", demoref->standings[i].timeorscore));
else
V_DrawRightAlignedString(BASEVIDWIDTH-40, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d'%02d\"%02d",
G_TicsToMinutes(demoref->standings[i].timeorscore, true),
G_TicsToSeconds(demoref->standings[i].timeorscore),
G_TicsToCentiseconds(demoref->standings[i].timeorscore)
));
else
V_DrawString(BASEVIDWIDTH-92, STARTY + i*20 + 9, V_SNAPTOTOP, va("%d", demoref->standings[i].timeorscore));
// Character face!
@ -4469,15 +4510,14 @@ void M_DrawAddons(void)
// Challenges Menu
#define challengesbordercolor 27
static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili)
{
unlockable_t *ref = NULL;
patch_t *pat = missingpat;
UINT8 *colormap = NULL;
fixed_t siz;
UINT8 id, num, work;
UINT8 id, num;
UINT32 edgelength;
id = (i * CHALLENGEGRIDHEIGHT) + j;
num = gamedata->challengegrid[id];
@ -4485,19 +4525,19 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili
// Empty spots in the grid are always unconnected.
if (num >= MAXUNLOCKABLES)
{
V_DrawFill(x, y, 16, 16, challengesbordercolor);
goto drawborder;
}
// Okay, this is what we want to draw.
ref = &unlockables[num];
edgelength = (ref->majorunlock ? 30 : 14);
// ...unless we simply aren't unlocked yet.
if ((gamedata->unlocked[num] == false)
|| (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME))
{
work = (ref->majorunlock) ? 2 : 1;
V_DrawFill(x, y, 16*work, 16*work,
V_DrawFill(x+1, y+1, edgelength, edgelength,
((challengesmenu.extradata[id] == CHE_HINT) ? 132 : 11));
goto drawborder;
}
@ -4548,6 +4588,12 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili
siz = (SHORT(pat->width) << FRACBITS);
siz = FixedDiv(((ref->majorunlock) ? 32 : 16) << FRACBITS, siz);
V_SetClipRect(
(x+1) << FRACBITS, (y+1) << FRACBITS,
edgelength << FRACBITS, edgelength << FRACBITS,
0
);
V_DrawFixedPatch(
x*FRACUNIT, y*FRACUNIT,
siz,
@ -4555,19 +4601,11 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili
colormap
);
V_ClearClipRect();
drawborder:
if (!hili)
{
if (ref != NULL)
{
work = 16 * (ref->majorunlock ? 2 : 1);
// Horizontal
V_DrawFill(x, y , work, 1, challengesbordercolor);
V_DrawFill(x, y + work-1, work, 1, challengesbordercolor);
// Vertical
V_DrawFill(x , y+1, 1, work-2, challengesbordercolor);
V_DrawFill(x + work-1, y+1, 1, work-2, challengesbordercolor);
}
return;
}
@ -4587,23 +4625,40 @@ static void M_DrawChallengePreview(INT32 x, INT32 y)
if (challengesmenu.currentunlock >= MAXUNLOCKABLES)
{
V_DrawFill(0, 146, BASEVIDWIDTH, 54, challengesbordercolor);
return;
}
// Okay, this is what we want to draw.
ref = &unlockables[challengesmenu.currentunlock];
// Funny question mark?
if (!gamedata->unlocked[challengesmenu.currentunlock])
{
// todo draw some sort of question mark?
V_DrawFill(0, 146, BASEVIDWIDTH, 54, challengesbordercolor);
spritedef_t *sprdef = &sprites[SPR_UQMK];
spriteframe_t *sprframe;
patch_t *patch;
UINT32 useframe;
UINT32 addflags = 0;
if (!sprdef->numframes)
{
return;
}
useframe = (challengesmenu.ticker / 2) % sprdef->numframes;
sprframe = &sprdef->spriteframes[useframe];
patch = W_CachePatchNum(sprframe->lumppat[0], PU_CACHE);
if (sprframe->flip & 1) // Only for first sprite
{
addflags ^= V_FLIP; // This sprite is left/right flipped!
}
V_DrawFixedPatch(x*FRACUNIT, (y+6)*FRACUNIT, FRACUNIT, addflags, patch, NULL);
return;
}
if (ref->type != SECRET_CUP)
V_DrawFill(0, 146, BASEVIDWIDTH, 54, challengesbordercolor);
switch (ref->type)
{
case SECRET_SKIN:
@ -4692,6 +4747,16 @@ static void M_DrawChallengePreview(INT32 x, INT32 y)
specialmap = btcmapcache;
break;
}
case SECRET_SPECIALATTACK:
{
static UINT16 sscmapcache = NEXTMAP_INVALID;
if (sscmapcache > nummapheaders)
{
sscmapcache = G_RandMap(G_TOLFlag(GT_SPECIAL), -1, 2, 0, false, NULL);
}
specialmap = sscmapcache;
break;
}
case SECRET_HARDSPEED:
{
static UINT16 hardmapcache = NEXTMAP_INVALID;
@ -4761,7 +4826,7 @@ void M_DrawChallenges(void)
INT16 offset;
{
patch_t *bg = W_CachePatchName("M_XTRABG", PU_CACHE);
patch_t *bg = W_CachePatchName("BGUNLCK2", PU_CACHE);
V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL);
}
@ -4796,15 +4861,10 @@ void M_DrawChallenges(void)
i = gamedata->challengegridwidth-1;
explodex = x - (i*16)/2;
x += (i*16)/2;
V_DrawFill(0, currentMenu->y, explodex, (CHALLENGEGRIDHEIGHT*16), challengesbordercolor);
V_DrawFill((x+16), currentMenu->y, BASEVIDWIDTH - (x+16), (CHALLENGEGRIDHEIGHT*16), challengesbordercolor);
}
selectx = explodex + (challengesmenu.hilix*16);
V_DrawFill(0, (currentMenu->y)-1 , BASEVIDWIDTH, 1, challengesbordercolor);
V_DrawFill(0, (currentMenu->y) + (CHALLENGEGRIDHEIGHT*16), BASEVIDWIDTH, 1, challengesbordercolor);
while (i >= 0 && x >= -32)
{
y = currentMenu->y-16;
@ -4859,7 +4919,6 @@ challengedesc:
// Name bar
{
y = 120;
V_DrawScaledPatch(0, y, 0, W_CachePatchName("MENUHINT", PU_CACHE));
if (challengesmenu.currentunlock < MAXUNLOCKABLES)
{
@ -4889,6 +4948,7 @@ challengedesc:
i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy;
if (challengesmenu.unlockcondition != NULL
&& challengesmenu.currentunlock < MAXUNLOCKABLES
&& ((gamedata->unlocked[challengesmenu.currentunlock] == true)
|| ((challengesmenu.extradata != NULL)
&& (challengesmenu.extradata[i] & CHE_HINT))

View file

@ -147,10 +147,6 @@ consvar_t cv_menujam_update = CVAR_INIT ("menujam_update", "Off", CV_SAVE, CV_On
static CV_PossibleValue_t menujam_cons_t[] = {{0, "menu"}, {1, "menu2"}, {2, "menu3"}, {0, NULL}};
static consvar_t cv_menujam = CVAR_INIT ("menujam", "0", CV_SAVE, menujam_cons_t, NULL);
// This gametype list is integral for many different reasons.
// When you add gametypes here, don't forget to update them in dehacked.c and doomstat.h!
CV_PossibleValue_t gametype_cons_t[NUMGAMETYPES+1];
static CV_PossibleValue_t serversort_cons_t[] = {
{0,"Ping"},
{1,"AVG. Power Level"},
@ -189,18 +185,18 @@ static CV_PossibleValue_t dummyteam_cons_t[] = {{0, "Spectator"}, {1, "Red"}, {2
static CV_PossibleValue_t dummyspectate_cons_t[] = {{0, "Spectator"}, {1, "Playing"}, {0, NULL}};
static CV_PossibleValue_t dummyscramble_cons_t[] = {{0, "Random"}, {1, "Points"}, {0, NULL}};
static CV_PossibleValue_t dummystaff_cons_t[] = {{0, "MIN"}, {100, "MAX"}, {0, NULL}};
static CV_PossibleValue_t dummygametype_cons_t[] = {{0, "Race"}, {1, "Battle"}, {0, NULL}};
//static consvar_t cv_dummymenuplayer = CVAR_INIT ("dummymenuplayer", "P1", CV_HIDDEN|CV_CALL, dummymenuplayer_cons_t, Dummymenuplayer_OnChange);
static consvar_t cv_dummyteam = CVAR_INIT ("dummyteam", "Spectator", CV_HIDDEN, dummyteam_cons_t, NULL);
//static cv_dummyspectate = CVAR_INITconsvar_t ("dummyspectate", "Spectator", CV_HIDDEN, dummyspectate_cons_t, NULL);
static consvar_t cv_dummyscramble = CVAR_INIT ("dummyscramble", "Random", CV_HIDDEN, dummyscramble_cons_t, NULL);
static consvar_t cv_dummystaff = CVAR_INIT ("dummystaff", "0", CV_HIDDEN|CV_CALL, dummystaff_cons_t, Dummystaff_OnChange);
consvar_t cv_dummygametype = CVAR_INIT ("dummygametype", "Race", CV_HIDDEN, dummygametype_cons_t, NULL);
consvar_t cv_dummyip = CVAR_INIT ("dummyip", "", CV_HIDDEN, NULL, NULL);
consvar_t cv_dummymenuplayer = CVAR_INIT ("dummymenuplayer", "P1", CV_HIDDEN|CV_CALL, dummymenuplayer_cons_t, Dummymenuplayer_OnChange);
consvar_t cv_dummyspectate = CVAR_INIT ("dummyspectate", "Spectator", CV_HIDDEN, dummyspectate_cons_t, NULL);
INT16 menugametype = GT_RACE;
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);
@ -1009,6 +1005,8 @@ void M_ClearMenus(boolean callexitmenufunc)
if (!menuactive)
return;
CON_ClearHUD();
if (currentMenu->quitroutine && callexitmenufunc && !currentMenu->quitroutine())
return; // we can't quit this menu (also used to set parameter from the menu)
@ -1221,18 +1219,33 @@ static boolean M_MenuConfirmPressed(UINT8 pid)
return M_MenuButtonPressed(pid, MBT_A);
}
/*static boolean M_MenuConfirmHeld(UINT8 pid)
{
return M_MenuButtonHeld(pid, MBT_A);
}*/
// Returns true if we press the Cancel button
static boolean M_MenuBackPressed(UINT8 pid)
{
return (M_MenuButtonPressed(pid, MBT_B) || M_MenuButtonPressed(pid, MBT_X));
}
/*static boolean M_MenuBackHeld(UINT8 pid)
{
return (M_MenuButtonHeld(pid, MBT_B) || M_MenuButtonHeld(pid, MBT_X));
}*/
// Retrurns true if we press the tertiary option button (C)
static boolean M_MenuExtraPressed(UINT8 pid)
{
return M_MenuButtonPressed(pid, MBT_C);
}
static boolean M_MenuExtraHeld(UINT8 pid)
{
return M_MenuButtonHeld(pid, MBT_C);
}
// Updates the x coordinate of the keybord so prevent it from going in weird places
static void M_UpdateKeyboardX(void)
@ -1713,7 +1726,6 @@ void M_Init(void)
CV_RegisterVar(&cv_dummyspectate);
CV_RegisterVar(&cv_dummyscramble);
CV_RegisterVar(&cv_dummystaff);
CV_RegisterVar(&cv_dummygametype);
CV_RegisterVar(&cv_dummyip);
CV_RegisterVar(&cv_dummyprofilename);
@ -3295,25 +3307,40 @@ void M_SetupGametypeMenu(INT32 choice)
PLAY_GamemodesDef.prevMenu = currentMenu;
// Battle and Capsules disabled
// 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 if (M_SecretUnlocked(SECRET_BREAKTHECAPSULES, true))
{
// Re-add Capsules
PLAY_GamemodesMenu[2].status = IT_STRING | IT_CALL;
}
else
{
// Only one non-Back entry, let's skip straight to Race.
M_SetupRaceMenu(-1);
return;
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);
@ -3406,9 +3433,6 @@ boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch)
if (mapheaderinfo[mapnum]->lumpnum == LUMPERROR)
return false;
if (levelsearch->checklocked && M_MapLocked(mapnum+1))
return false; // not unlocked
// Check for TOL
if (!(mapheaderinfo[mapnum]->typeoflevel & levelsearch->typeoflevel))
return false;
@ -3427,6 +3451,19 @@ boolean M_CanShowLevelInList(INT16 mapnum, levelsearch_t *levelsearch)
&& mapheaderinfo[mapnum]->cup != levelsearch->cup)
return false;
// Finally, the most complex check: does the map have lock conditions?
if (levelsearch->checklocked)
{
// Check for completion
if ((mapheaderinfo[mapnum]->menuflags & LF2_FINISHNEEDED)
&& !(mapheaderinfo[mapnum]->mapvisited & MV_BEATEN))
return false;
// Check for unlock
if (M_MapLocked(mapnum+1))
return false;
}
// Survived our checks.
return true;
}
@ -3440,6 +3477,9 @@ UINT16 M_CountLevelsToShowInList(levelsearch_t *levelsearch)
if (levelsearch->cup)
{
if (levelsearch->checklocked && M_CupLocked(levelsearch->cup))
return 0;
for (i = 0; i < CUPCACHE_MAX; i++)
{
if (!M_CanShowLevelInList(levelsearch->cup->cachedlevels[i], levelsearch))
@ -3466,6 +3506,12 @@ UINT16 M_GetFirstLevelInList(UINT8 *i, levelsearch_t *levelsearch)
if (levelsearch->cup)
{
if (levelsearch->checklocked && M_CupLocked(levelsearch->cup))
{
*i = CUPCACHE_MAX;
return NEXTMAP_INVALID;
}
*i = 0;
mapnum = NEXTMAP_INVALID;
for (; *i < CUPCACHE_MAX; (*i)++)
@ -3530,20 +3576,39 @@ static void M_LevelSelectScrollDest(void)
}
// Builds the level list we'll be using from the gametype we're choosing and send us to the apropriate menu.
static void M_LevelListFromGametype(INT16 gt)
static boolean M_LevelListFromGametype(INT16 gt)
{
static boolean first = true;
if (first || gt != levellist.newgametype)
UINT8 temp = 0;
if (first || gt != levellist.newgametype || levellist.guessgt != MAXGAMETYPES)
{
if (first)
{
cupgrid.cappages = 0;
cupgrid.builtgrid = NULL;
}
levellist.newgametype = gt;
levellist.levelsearch.typeoflevel = G_TOLFlag(gt);
levellist.levelsearch.cupmode = (!(gametypedefaultrules[gt] & GTR_NOCUPSELECT));
if (levellist.levelsearch.timeattack == true && gt == GT_SPECIAL)
{
// Sneak in an extra.
levellist.levelsearch.typeoflevel |= G_TOLFlag(GT_VERSUS);
levellist.guessgt = gt;
}
else
{
levellist.guessgt = MAXGAMETYPES;
}
levellist.levelsearch.cupmode = (!(gametypes[gt]->rules & GTR_NOCUPSELECT));
levellist.levelsearch.cup = NULL;
first = false;
}
PLAY_CupSelectDef.prevMenu = currentMenu;
// Obviously go to Cup Select in gametypes that have cups.
// Use a really long level select in gametypes that don't use cups.
@ -3551,32 +3616,35 @@ static void M_LevelListFromGametype(INT16 gt)
{
levelsearch_t templevelsearch = levellist.levelsearch; // full copy
size_t currentid = 0, highestunlockedid = 0;
const size_t unitlen = sizeof(cupheader_t*) * (CUPMENU_COLUMNS * CUPMENU_ROWS);
const size_t pagelen = sizeof(cupheader_t*) * (CUPMENU_COLUMNS * CUPMENU_ROWS);
boolean foundany = false;
templevelsearch.cup = kartcupheaders;
templevelsearch.checklocked = false;
// Make sure there's valid cups before going to this menu.
#if 0
// Make sure there's valid cups before going to this menu. -- rip sweet prince
if (templevelsearch.cup == NULL)
I_Error("Can you really call this a racing game, I didn't recieve any Cups on my pillow or anything");
#endif
if (!cupgrid.builtgrid)
if (cupgrid.cappages == 0)
{
cupgrid.cappages = 2;
cupgrid.builtgrid = Z_Calloc(
cupgrid.cappages * unitlen,
cupgrid.cappages * pagelen,
PU_STATIC,
cupgrid.builtgrid);
NULL);
if (!cupgrid.builtgrid)
{
I_Error("M_LevelListFromGametype: Not enough memory to allocate builtgrid");
}
}
memset(cupgrid.builtgrid, 0, cupgrid.cappages * unitlen);
memset(cupgrid.builtgrid, 0, cupgrid.cappages * pagelen);
while (templevelsearch.cup)
{
templevelsearch.checklocked = false;
if (!M_CountLevelsToShowInList(&templevelsearch))
{
// No valid maps, skip.
@ -3584,10 +3652,12 @@ static void M_LevelListFromGametype(INT16 gt)
continue;
}
if ((currentid * sizeof(cupheader_t*)) >= cupgrid.cappages * unitlen)
foundany = true;
if ((currentid * sizeof(cupheader_t*)) >= cupgrid.cappages * pagelen)
{
// Double the size of the buffer, and clear the other stuff.
const size_t firstlen = cupgrid.cappages * unitlen;
const size_t firstlen = cupgrid.cappages * pagelen;
cupgrid.builtgrid = Z_Realloc(cupgrid.builtgrid,
firstlen * 2,
PU_STATIC, NULL);
@ -3603,7 +3673,8 @@ static void M_LevelListFromGametype(INT16 gt)
cupgrid.builtgrid[currentid] = templevelsearch.cup;
if (!M_CupLocked(templevelsearch.cup))
templevelsearch.checklocked = true;
if (M_GetFirstLevelInList(&temp, &templevelsearch) != NEXTMAP_INVALID)
{
highestunlockedid = currentid;
if (Playing() && mapheaderinfo[gamemap-1] && mapheaderinfo[gamemap-1]->cup == templevelsearch.cup)
@ -3618,16 +3689,29 @@ static void M_LevelListFromGametype(INT16 gt)
templevelsearch.cup = templevelsearch.cup->next;
}
if (foundany == false)
{
return false;
}
cupgrid.numpages = (highestunlockedid / (CUPMENU_COLUMNS * CUPMENU_ROWS)) + 1;
if (cupgrid.pageno >= cupgrid.numpages)
{
cupgrid.pageno = 0;
}
PLAY_CupSelectDef.prevMenu = currentMenu;
PLAY_LevelSelectDef.prevMenu = &PLAY_CupSelectDef;
M_SetupNextMenu(&PLAY_CupSelectDef, false);
return;
return true;
}
// Okay, just a list of maps then.
if (M_GetFirstLevelInList(&temp, &levellist.levelsearch) == NEXTMAP_INVALID)
{
return false;
}
// Reset position properly if you go back & forth between gametypes
@ -3643,6 +3727,7 @@ static void M_LevelListFromGametype(INT16 gt)
PLAY_LevelSelectDef.prevMenu = currentMenu;
M_SetupNextMenu(&PLAY_LevelSelectDef, false);
return true;
}
// Init level select for use in local play using the last choice we made.
@ -3651,10 +3736,11 @@ static void M_LevelListFromGametype(INT16 gt)
void M_LevelSelectInit(INT32 choice)
{
INT32 gt = currentMenu->menuitems[itemOn].mvar2;
(void)choice;
// Make sure this is reset as we'll only be using this function for offline games!
cupgrid.netgame = false;
levellist.netgame = false;
levellist.levelsearch.checklocked = true;
@ -3677,7 +3763,111 @@ void M_LevelSelectInit(INT32 choice)
return;
}
M_LevelListFromGametype(currentMenu->menuitems[itemOn].mvar2);
if (gt == -1)
{
gt = menugametype;
}
if (!M_LevelListFromGametype(gt))
{
S_StartSound(NULL, sfx_s3kb2);
M_StartMessage(va("No levels available for\n%s Mode!\n\nPress (B)\n", gametypes[gt]->name), NULL, MM_NOTHING);
}
}
static void M_LevelSelected(INT16 add)
{
UINT8 i = 0;
INT16 map = M_GetFirstLevelInList(&i, &levellist.levelsearch);
while (add > 0)
{
map = M_GetNextLevelInList(map, &i, &levellist.levelsearch);
if (map >= nummapheaders)
{
break;
}
add--;
}
if (map >= nummapheaders)
{
// This shouldn't happen
return;
}
levellist.choosemap = map;
if (levellist.levelsearch.timeattack)
{
S_StartSound(NULL, sfx_s3k63);
if (levellist.guessgt != MAXGAMETYPES)
levellist.newgametype = G_GuessGametypeByTOL(levellist.levelsearch.typeoflevel);
PLAY_TimeAttackDef.lastOn = ta_start;
PLAY_TimeAttackDef.prevMenu = currentMenu;
M_SetupNextMenu(&PLAY_TimeAttackDef, false);
}
else
{
if (gamestate == GS_MENU)
{
UINT8 ssplayers = cv_splitplayers.value-1;
netgame = false;
multiplayer = true;
strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
// Still need to reset devmode
cht_debug = 0;
if (demo.playback)
G_StopDemo();
if (metalrecording)
G_StopMetalDemo();
/*if (levellist.choosemap == 0)
levellist.choosemap = G_RandMap(G_TOLFlag(levellist.newgametype), -1, 0, 0, false, NULL);*/
if (cv_maxconnections.value < ssplayers+1)
CV_SetValue(&cv_maxconnections, ssplayers+1);
if (splitscreen != ssplayers)
{
splitscreen = ssplayers;
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, levellist.netgame);
CV_StealthSet(&cv_kartbot, cv_dummymatchbots.string);
CV_StealthSet(&cv_kartencore, (cv_dummygpencore.value == 1) ? "On" : "Auto");
CV_StealthSet(&cv_kartspeed, (cv_dummykartspeed.value == KARTSPEED_NORMAL) ? "Auto" : cv_dummykartspeed.string);
D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_kartencore.value == 1), 1, 1, false, false);
}
else
{
// directly do the map change
D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_kartencore.value == 1), 1, 1, false, false);
}
M_ClearMenus(true);
}
}
void M_CupSelectHandler(INT32 choice)
@ -3733,13 +3923,18 @@ void M_CupSelectHandler(INT32 choice)
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)
|| (M_CupLocked(newcup))
|| (newcup->cachedlevels[0] == NEXTMAP_INVALID))
|| (count <= 0)
|| (cupgrid.grandprix == true && newcup->cachedlevels[0] == NEXTMAP_INVALID))
{
S_StartSound(NULL, sfx_s3kb2);
return;
@ -3803,13 +3998,17 @@ void M_CupSelectHandler(INT32 choice)
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 (levellist.levelsearch.cup != newcup)
if (oldcup != newcup || levellist.cursor >= count)
{
levellist.cursor = 0;
levellist.levelsearch.cup = newcup;
}
M_LevelSelectScrollDest();
@ -3868,94 +4067,10 @@ void M_LevelSelectHandler(INT32 choice)
if (M_MenuConfirmPressed(pid) /*|| M_MenuButtonPressed(pid, MBT_START)*/)
{
UINT8 i = 0;
INT16 map = M_GetFirstLevelInList(&i, &levellist.levelsearch);
INT16 add = levellist.cursor;
M_SetMenuDelay(pid);
while (add > 0)
{
map = M_GetNextLevelInList(map, &i, &levellist.levelsearch);
if (map >= nummapheaders)
{
break;
}
add--;
}
if (map >= nummapheaders)
{
// This shouldn't happen
return;
}
levellist.choosemap = map;
if (levellist.levelsearch.timeattack)
{
M_SetupNextMenu(&PLAY_TimeAttackDef, false);
S_StartSound(NULL, sfx_s3k63);
}
else
{
if (gamestate == GS_MENU)
{
UINT8 ssplayers = cv_splitplayers.value-1;
netgame = false;
multiplayer = true;
strncpy(connectedservername, cv_servername.string, MAXSERVERNAME);
// Still need to reset devmode
cht_debug = 0;
if (demo.playback)
G_StopDemo();
if (metalrecording)
G_StopMetalDemo();
/*if (levellist.choosemap == 0)
levellist.choosemap = G_RandMap(G_TOLFlag(levellist.newgametype), -1, 0, 0, false, NULL);*/
if (cv_maxconnections.value < ssplayers+1)
CV_SetValue(&cv_maxconnections, ssplayers+1);
if (splitscreen != ssplayers)
{
splitscreen = ssplayers;
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, levellist.netgame);
CV_StealthSet(&cv_kartbot, cv_dummymatchbots.string);
CV_StealthSet(&cv_kartencore, (cv_dummygpencore.value == 1) ? "On" : "Auto");
CV_StealthSet(&cv_kartspeed, (cv_dummykartspeed.value == KARTSPEED_NORMAL) ? "Auto" : cv_dummykartspeed.string);
D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_kartencore.value == 1), 1, 1, false, false);
}
else
{
// directly do the map change
D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_kartencore.value == 1), 1, 1, false, false);
}
M_ClearMenus(true);
}
PLAY_TimeAttackDef.transitionID = currentMenu->transitionID;
M_LevelSelected(levellist.cursor);
}
else if (M_MenuBackPressed(pid))
{
@ -4006,14 +4121,12 @@ void M_StartTimeAttack(INT32 choice)
(void)choice;
switch (levellist.newgametype)
modeattacking = ATTACKING_TIME;
if ((gametypes[levellist.newgametype]->rules & GTR_CIRCUIT)
&& (mapheaderinfo[levellist.choosemap]->numlaps != 1))
{
case GT_BATTLE:
modeattacking = ATTACKING_CAPSULES;
break;
default:
modeattacking = ATTACKING_TIME;
break;
modeattacking |= ATTACKING_LAP;
}
// Still need to reset devmode
@ -4078,6 +4191,7 @@ 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;
@ -4088,6 +4202,10 @@ void M_MPOptSelectInit(INT32 choice)
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);
}
@ -4119,41 +4237,93 @@ void M_MPHostInit(INT32 choice)
itemOn = mhost_go;
}
void M_NextMenuGametype(UINT32 forbidden)
{
const INT16 currentmenugametype = menugametype;
do
{
menugametype++;
if (menugametype >= numgametypes)
menugametype = 0;
if (!(gametypes[menugametype]->rules & forbidden))
break;
} while (menugametype != currentmenugametype);
}
void M_PrevMenuGametype(UINT32 forbidden)
{
const INT16 currentmenugametype = menugametype;
do
{
if (menugametype == 0)
menugametype = numgametypes;
menugametype--;
if (!(gametypes[menugametype]->rules & forbidden))
break;
} while (menugametype != currentmenugametype);
}
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)
{
INT16 gt = GT_RACE;
(void)choice;
// Yep, we'll be starting a netgame.
levellist.netgame = true;
cupgrid.netgame = true;
// Make sure we reset those
levellist.levelsearch.timeattack = false;
levellist.levelsearch.checklocked = true;
cupgrid.grandprix = false;
// In case we ever want to add new gamemodes there somehow, have at it!
switch (cv_dummygametype.value)
{
case 1: // Battle
{
gt = GT_BATTLE;
break;
}
default:
{
gt = GT_RACE;
break;
}
}
// okay this is REALLY stupid but this fixes the host menu re-folding on itself when we go back.
mpmenu.modewinextend[0][0] = 1;
M_LevelListFromGametype(gt); // Setup the level select.
// (This will also automatically send us to the apropriate menu)
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);
}
}
// MULTIPLAYER JOIN BY IP
@ -6063,6 +6233,7 @@ void M_OpenPauseMenu(void)
// By default, disable anything sensitive:
PAUSE_Main[mpause_addons].status = IT_DISABLED;
PAUSE_Main[mpause_changegametype].status = IT_DISABLED;
PAUSE_Main[mpause_switchmap].status = IT_DISABLED;
PAUSE_Main[mpause_restartmap].status = IT_DISABLED;
PAUSE_Main[mpause_tryagain].status = IT_DISABLED;
@ -6084,14 +6255,10 @@ void M_OpenPauseMenu(void)
if (server || IsPlayerAdmin(consoleplayer))
{
PAUSE_Main[mpause_switchmap].status = IT_STRING | IT_SUBMENU;
for (i = 0; i < PAUSE_GamemodesDef.numitems; i++)
{
if (PAUSE_GamemodesMenu[i].mvar2 != gametype)
continue;
PAUSE_GamemodesDef.lastOn = i;
break;
}
PAUSE_Main[mpause_changegametype].status = IT_STRING | IT_KEYHANDLER;
menugametype = gametype;
PAUSE_Main[mpause_switchmap].status = IT_STRING | IT_CALL;
PAUSE_Main[mpause_restartmap].status = IT_STRING | IT_CALL;
PAUSE_Main[mpause_addons].status = IT_STRING | IT_CALL;
}
@ -6192,6 +6359,46 @@ boolean M_PauseInputs(INT32 ch)
return false;
}
// Change gametype
void M_HandlePauseMenuGametype(INT32 choice)
{
const UINT8 pid = 0;
const UINT32 forbidden = GTR_FORBIDMP;
(void)choice;
if (M_MenuConfirmPressed(pid))
{
if (menugametype != gametype)
{
M_ClearMenus(true);
COM_ImmedExecute(va("randommap -gt %s", gametypes[menugametype]->name));
return;
}
M_SetMenuDelay(pid);
S_StartSound(NULL, sfx_s3k7b);
}
else if (M_MenuExtraPressed(pid))
{
menugametype = gametype;
M_SetMenuDelay(pid);
S_StartSound(NULL, sfx_s3k7b);
}
else if (menucmd[pid].dpad_lr > 0)
{
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);
}
}
// Restart map
void M_RestartMap(INT32 choice)
{
@ -6269,7 +6476,7 @@ void M_EndGame(INT32 choice)
if (!Playing())
return;
M_StartMessage(M_GetText("Are you sure you want to return\nto the title screen?\nPress (A) to confirm or (B) to cancel\n"), FUNCPTRCAST(M_ExitGameResponse), MM_YESNO);
M_StartMessage(M_GetText("Are you sure you want to\nreturn to the menu?\nPress (A) to confirm or (B) to cancel\n"), FUNCPTRCAST(M_ExitGameResponse), MM_YESNO);
}
@ -6429,7 +6636,9 @@ void M_PrepReplayList(void)
else
{
extrasmenu.demolist[i].type = MD_NOTLOADED;
snprintf(extrasmenu.demolist[i].filepath, 255, "%s%s", menupath, dirmenu[i] + DIR_STRING);
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, ".....");
}
}
@ -6650,7 +6859,7 @@ void M_Addons(INT32 choice)
#if 1
if (cv_addons_option.value == 0)
pathname = usehome ? srb2home : srb2path;
pathname = addonsdir;
else if (cv_addons_option.value == 1)
pathname = srb2home;
else if (cv_addons_option.value == 2)
@ -7177,6 +7386,7 @@ void M_Challenges(INT32 choice)
void M_ChallengesTick(void)
{
const UINT8 pid = 0;
UINT8 i, newunlock = MAXUNLOCKABLES;
boolean fresh = (challengesmenu.currentunlock >= MAXUNLOCKABLES);
@ -7220,8 +7430,9 @@ void M_ChallengesTick(void)
else
{
// Unlock sequence.
tic_t nexttime = M_MenuExtraHeld(pid) ? (UNLOCKTIME*2) : MAXUNLOCKTIME;
if (++challengesmenu.unlockanim >= MAXUNLOCKTIME)
if (++challengesmenu.unlockanim >= nexttime)
{
challengesmenu.requestnew = true;
}
@ -7546,9 +7757,16 @@ void M_Statistics(INT32 choice)
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;

View file

@ -60,7 +60,7 @@ void Obj_DuelBombInit(mobj_t *bomb);
/* Broly Ki */
mobj_t *Obj_SpawnBrolyKi(mobj_t *source, tic_t duration);
void Obj_BrolyKiThink(mobj_t *ki);
boolean Obj_BrolyKiThink(mobj_t *ki);
/* Special Stage UFO */
waypoint_t *K_GetSpecialUFOWaypoint(mobj_t *ufo);
@ -73,6 +73,22 @@ void Obj_UFOPieceRemoved(mobj_t *piece);
mobj_t *Obj_CreateSpecialUFO(void);
UINT32 K_GetSpecialUFODistance(void);
/* Monitors */
mobj_t *Obj_SpawnMonitor(mobj_t *origin, UINT8 numItemTypes, UINT8 emerald);
void Obj_MonitorSpawnParts(mobj_t *monitor);
void Obj_MonitorPartThink(mobj_t *part);
fixed_t Obj_MonitorGetDamage(mobj_t *monitor, mobj_t *inflictor, UINT8 damagetype);
void Obj_MonitorOnDamage(mobj_t *monitor, mobj_t *inflictor, INT32 damage);
void Obj_MonitorOnDeath(mobj_t *monitor);
void Obj_MonitorShardThink(mobj_t *shard);
UINT32 Obj_MonitorGetEmerald(const mobj_t *monitor);
void Obj_MonitorSetItemSpot(mobj_t *monitor, mobj_t *spot);
/* Item Spot */
boolean Obj_ItemSpotIsAvailable(const mobj_t *spot);
void Obj_ItemSpotAssignMonitor(mobj_t *spot, mobj_t *monitor);
void Obj_ItemSpotUpdate(mobj_t *spot);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -217,16 +217,13 @@ void PR_SaveProfiles(void)
size_t length = 0;
const size_t headerlen = strlen(PROFILEHEADER);
UINT8 i, j, k;
savebuffer_t save;
savebuffer_t save = {0};
save.size = sizeof(UINT32) + (numprofiles * sizeof(profile_t));
save.p = save.buffer = (UINT8 *)malloc(save.size);
if (!save.p)
if (P_SaveBufferAlloc(&save, sizeof(UINT32) + (numprofiles * sizeof(profile_t))) == false)
{
I_Error("No more free memory for saving profiles\n");
return;
}
save.end = save.buffer + save.size;
// Add header.
WRITESTRINGN(save.p, PROFILEHEADER, headerlen);
@ -270,15 +267,14 @@ void PR_SaveProfiles(void)
if (!FIL_WriteFile(va(pandf, srb2home, PROFILESFILE), save.buffer, length))
{
free(save.buffer);
P_SaveBufferFree(&save);
I_Error("Couldn't save profiles. Are you out of Disk space / playing in a protected folder?");
}
free(save.buffer);
P_SaveBufferFree(&save);
}
void PR_LoadProfiles(void)
{
size_t length = 0;
const size_t headerlen = strlen(PROFILEHEADER);
UINT8 i, j, k, version;
profile_t *dprofile = PR_MakeProfile(
@ -289,26 +285,22 @@ void PR_LoadProfiles(void)
gamecontroldefault,
true
);
savebuffer_t save;
savebuffer_t save = {0};
length = FIL_ReadFile(va(pandf, srb2home, PROFILESFILE), &save.buffer);
if (!length)
if (P_SaveBufferFromFile(&save, va(pandf, srb2home, PROFILESFILE)) == false)
{
// No profiles. Add the default one.
PR_AddProfile(dprofile);
return;
}
save.p = save.buffer;
if (strncmp(PROFILEHEADER, (const char *)save.buffer, headerlen))
{
const char *gdfolder = "the Ring Racers folder";
if (strcmp(srb2home,"."))
gdfolder = srb2home;
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
I_Error("Not a valid Profile file.\nDelete %s (maybe in %s) and try again.", PROFILESFILE, gdfolder);
}
save.p += headerlen;
@ -316,8 +308,7 @@ void PR_LoadProfiles(void)
version = READUINT8(save.p);
if (version > PROFILEVER)
{
Z_Free(save.buffer);
save.p = NULL;
P_SaveBufferFree(&save);
I_Error("Existing %s is from the future! (expected %d, got %d)", PROFILESFILE, PROFILEVER, version);
}

View file

@ -16,7 +16,6 @@
#include "m_cond.h" // M_UpdateUnlockablesAndExtraEmblems
#include "p_tick.h" // leveltime
#include "k_grandprix.h"
#include "k_boss.h"
#include "k_profiles.h"
// Client-sided calculations done for Power Levels.
@ -38,7 +37,7 @@ SINT8 K_UsingPowerLevels(void)
{
SINT8 pt = PWRLV_DISABLED;
if (!cv_kartusepwrlv.value || !(netgame || (demo.playback && demo.netgame)) || grandprixinfo.gp == true || bossinfo.boss == true)
if (!cv_kartusepwrlv.value || !(netgame || (demo.playback && demo.netgame)) || grandprixinfo.gp == true)
{
return PWRLV_DISABLED;
}

View file

@ -400,7 +400,7 @@ static void K_DrawFinishLineBeamForLine(fixed_t offset, angle_t aiming, line_t *
void K_RunFinishLineBeam(void)
{
if (!(leveltime < starttime || rainbowstartavailable == true))
if ((gametyperules & GTR_ROLLINGSTART) || !(leveltime < starttime || rainbowstartavailable == true))
{
return;
}

View file

@ -175,14 +175,14 @@ void K_DoIngameRespawn(player_t *player)
mapthing_t *beststart = NULL;
UINT8 numstarts = 0;
if (gametype == GT_RACE)
{
numstarts = numcoopstarts;
}
else if (gametype == GT_BATTLE)
if (gametyperules & GTR_BATTLESTARTS)
{
numstarts = numdmstarts;
}
else
{
numstarts = numcoopstarts;
}
if (numstarts > 0)
{
@ -193,17 +193,13 @@ void K_DoIngameRespawn(player_t *player)
UINT32 dist = UINT32_MAX;
mapthing_t *checkstart = NULL;
if (gametype == GT_RACE)
{
checkstart = playerstarts[i];
}
else if (gametype == GT_BATTLE)
if (gametyperules & GTR_BATTLESTARTS)
{
checkstart = deathmatchstarts[i];
}
else
{
break;
checkstart = playerstarts[i];
}
dist = (UINT32)P_AproxDistance((player->mo->x >> FRACBITS) - checkstart->x,

View file

@ -176,6 +176,12 @@ static UINT8 K_KartItemOddsSpecial[NUMKARTRESULTS-1][4] =
{ 0, 0, 0, 0 } // Gachabom x3
};
static kartitems_t K_KartItemReelSpecialEnd[] =
{
KITEM_SUPERRING,
KITEM_NONE
};
static kartitems_t K_KartItemReelTimeAttack[] =
{
KITEM_SNEAKER,
@ -359,7 +365,7 @@ static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers
return 0;
}
if (specialStage.active == true)
if (specialstageinfo.valid == true)
{
UINT32 ufoDis = K_GetSpecialUFODistance();
@ -411,6 +417,171 @@ static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers
return pdis;
}
/*--------------------------------------------------
static boolean K_DenyShieldOdds(kartitems_t item)
Checks if this type of shield already exists in
another player's inventory.
Input Arguments:-
item - The item type of the shield.
Return:-
Whether this item is a shield and may not be awarded
at this time.
--------------------------------------------------*/
static boolean K_DenyShieldOdds(kartitems_t item)
{
INT32 shieldType = K_GetShieldFromItem(item);
if ((gametyperules & GTR_CIRCUIT) == 0)
{
return false;
}
switch (shieldType)
{
case KSHIELD_NONE:
/* Marble Garden Top is not REALLY
a Sonic 3 shield */
case KSHIELD_TOP:
{
break;
}
default:
{
size_t i;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] == false || players[i].spectator == true)
{
continue;
}
if (shieldType == K_GetShieldFromItem(players[i].itemtype))
{
// Don't allow more than one of each shield type at a time
return true;
}
}
}
}
return false;
}
/*--------------------------------------------------
static fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position)
Adjust odds of SPB according to distances of first and
second place players.
Input Arguments:-
roulette - The roulette data that we intend to
insert this item into.
position - Position of player to consider for these
odds.
Return:-
New item odds.
--------------------------------------------------*/
static fixed_t K_AdjustSPBOdds(const itemroulette_t *roulette, UINT8 position)
{
I_Assert(roulette != NULL);
if (roulette->firstDist < ENDDIST*2 // No SPB when 1st is almost done
|| position == 1) // No SPB for 1st ever
{
return 0;
}
else
{
const UINT32 dist = max(0, ((signed)roulette->secondToFirst) - SPBSTARTDIST);
const UINT32 distRange = SPBFORCEDIST - SPBSTARTDIST;
const fixed_t maxOdds = 20 << FRACBITS;
fixed_t multiplier = FixedDiv(dist, distRange);
if (multiplier < 0)
{
multiplier = 0;
}
if (multiplier > FRACUNIT)
{
multiplier = FRACUNIT;
}
return FixedMul(maxOdds, multiplier);
}
}
typedef struct {
boolean powerItem;
boolean cooldownOnStart;
boolean notNearEnd;
// gameplay state
boolean rival; // player is a bot Rival
} itemconditions_t;
/*--------------------------------------------------
static fixed_t K_AdjustItemOddsToConditions(fixed_t newOdds, const itemconditions_t *conditions, const itemroulette_t *roulette)
Adjust item odds to certain group conditions.
Input Arguments:-
newOdds - The item odds to adjust.
conditions - The conditions state.
roulette - The roulette data that we intend to
insert this item into.
Return:-
New item odds.
--------------------------------------------------*/
static fixed_t K_AdjustItemOddsToConditions(fixed_t newOdds, const itemconditions_t *conditions, const itemroulette_t *roulette)
{
// None if this applies outside of Race modes (for now?)
if ((gametyperules & GTR_CIRCUIT) == 0)
{
return newOdds;
}
if ((conditions->cooldownOnStart == true) && (leveltime < (30*TICRATE) + starttime))
{
// This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items)
newOdds = 0;
}
else if ((conditions->notNearEnd == true) && (roulette != NULL && roulette->baseDist < ENDDIST))
{
// This item should not appear at the end of a race. (Usually trap items that lose their effectiveness)
newOdds = 0;
}
else if (conditions->powerItem == true)
{
// This item is a "power item". This activates "frantic item" toggle related functionality.
if (franticitems == true)
{
// First, power items multiply their odds by 2 if frantic items are on; easy-peasy.
newOdds *= 2;
}
if (conditions->rival == true)
{
// The Rival bot gets frantic-like items, also :p
newOdds *= 2;
}
if (roulette != NULL)
{
newOdds = FixedMul(newOdds, FRACUNIT + K_ItemOddsScale(roulette->playing));
}
}
return newOdds;
}
/*--------------------------------------------------
INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item)
@ -419,19 +590,16 @@ static UINT32 K_GetItemRouletteDistance(const player_t *player, UINT8 numPlayers
INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette, UINT8 pos, kartitems_t item)
{
boolean bot = false;
boolean rival = false;
UINT8 position = 0;
INT32 shieldType = KSHIELD_NONE;
boolean powerItem = false;
boolean cooldownOnStart = false;
boolean notNearEnd = false;
itemconditions_t conditions = {
.powerItem = false,
.cooldownOnStart = false,
.notNearEnd = false,
.rival = false,
};
fixed_t newOdds = 0;
size_t i;
I_Assert(roulette != NULL);
I_Assert(item > KITEM_NONE); // too many off by one scenarioes.
I_Assert(item < NUMKARTRESULTS);
@ -439,7 +607,7 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
if (player != NULL)
{
bot = player->bot;
rival = (bot == true && player->botvars.rival == true);
conditions.rival = (bot == true && player->botvars.rival == true);
position = player->position;
}
@ -472,33 +640,9 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
*/
(void)bot;
shieldType = K_GetShieldFromItem(item);
switch (shieldType)
if (K_DenyShieldOdds(item))
{
case KSHIELD_NONE:
/* Marble Garden Top is not REALLY
a Sonic 3 shield */
case KSHIELD_TOP:
{
break;
}
default:
{
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] == false || players[i].spectator == true)
{
continue;
}
if (shieldType == K_GetShieldFromItem(players[i].itemtype))
{
// Don't allow more than one of each shield type at a time
return 0;
}
}
}
return 0;
}
if (gametype == GT_BATTLE)
@ -506,7 +650,7 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
I_Assert(pos < 2); // DO NOT allow positions past the bounds of the table
newOdds = K_KartItemOddsBattle[item-1][pos];
}
else if (specialStage.active == true)
else if (specialstageinfo.valid == true)
{
I_Assert(pos < 4); // Ditto
newOdds = K_KartItemOddsSpecial[item-1][pos];
@ -525,7 +669,7 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
case KITEM_EGGMAN:
case KITEM_SUPERRING:
{
notNearEnd = true;
conditions.notNearEnd = true;
break;
}
@ -539,15 +683,15 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
case KRITEM_QUADORBINAUT:
case KRITEM_DUALJAWZ:
{
powerItem = true;
conditions.powerItem = true;
break;
}
case KITEM_HYUDORO:
case KRITEM_TRIPLEBANANA:
{
powerItem = true;
notNearEnd = true;
conditions.powerItem = true;
conditions.notNearEnd = true;
break;
}
@ -557,59 +701,34 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
case KITEM_BUBBLESHIELD:
case KITEM_FLAMESHIELD:
{
cooldownOnStart = true;
powerItem = true;
conditions.cooldownOnStart = true;
conditions.powerItem = true;
break;
}
case KITEM_SPB:
{
cooldownOnStart = true;
notNearEnd = true;
conditions.cooldownOnStart = true;
conditions.notNearEnd = true;
if ((gametyperules & GTR_CIRCUIT) == 0)
if (roulette != NULL &&
(gametyperules & GTR_CIRCUIT) &&
specialstageinfo.valid == false)
{
// Needs to be a race.
return 0;
}
if (specialStage.active == false)
{
if (roulette->firstDist < ENDDIST*2 // No SPB when 1st is almost done
|| position == 1) // No SPB for 1st ever
{
return 0;
}
else
{
const UINT32 dist = max(0, ((signed)roulette->secondToFirst) - SPBSTARTDIST);
const UINT32 distRange = SPBFORCEDIST - SPBSTARTDIST;
const fixed_t maxOdds = 20 << FRACBITS;
fixed_t multiplier = FixedDiv(dist, distRange);
if (multiplier < 0)
{
multiplier = 0;
}
if (multiplier > FRACUNIT)
{
multiplier = FRACUNIT;
}
newOdds = FixedMul(maxOdds, multiplier);
}
newOdds = K_AdjustSPBOdds(roulette, position);
}
break;
}
case KITEM_SHRINK:
{
cooldownOnStart = true;
powerItem = true;
notNearEnd = true;
conditions.cooldownOnStart = true;
conditions.powerItem = true;
conditions.notNearEnd = true;
if (roulette->playing - 1 <= roulette->exiting)
if (roulette != NULL &&
(gametyperules & GTR_CIRCUIT) &&
roulette->playing - 1 <= roulette->exiting)
{
return 0;
}
@ -618,10 +737,10 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
case KITEM_LIGHTNINGSHIELD:
{
cooldownOnStart = true;
powerItem = true;
conditions.cooldownOnStart = true;
conditions.powerItem = true;
if (spbplace != -1)
if ((gametyperules & GTR_CIRCUIT) && spbplace != -1)
{
return 0;
}
@ -640,35 +759,7 @@ INT32 K_KartGetItemOdds(const player_t *player, itemroulette_t *const roulette,
return newOdds;
}
if ((cooldownOnStart == true) && (leveltime < (30*TICRATE)+starttime))
{
// This item should not appear at the beginning of a race. (Usually really powerful crowd-breaking items)
newOdds = 0;
}
else if ((notNearEnd == true) && (roulette->baseDist < ENDDIST))
{
// This item should not appear at the end of a race. (Usually trap items that lose their effectiveness)
newOdds = 0;
}
else if (powerItem == true)
{
// This item is a "power item". This activates "frantic item" toggle related functionality.
if (franticitems == true)
{
// First, power items multiply their odds by 2 if frantic items are on; easy-peasy.
newOdds *= 2;
}
if (rival == true)
{
// The Rival bot gets frantic-like items, also :p
newOdds *= 2;
}
newOdds = FixedMul(newOdds, FRACUNIT + K_ItemOddsScale(roulette->playing));
}
newOdds = FixedInt(FixedRound(newOdds));
newOdds = FixedInt(FixedRound(K_AdjustItemOddsToConditions(newOdds, &conditions, roulette)));
return newOdds;
}
@ -705,7 +796,7 @@ static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulett
oddsValid[i] = false;
continue;
}
else if (specialStage.active == true && i > 3)
else if (specialstageinfo.valid == true && i > 3)
{
oddsValid[i] = false;
continue;
@ -734,7 +825,7 @@ static UINT8 K_FindUseodds(const player_t *player, itemroulette_t *const roulett
}
else
{
if (specialStage.active == true) // Special Stages
if (specialstageinfo.valid == true) // Special Stages
{
SETUPDISTTABLE(0,2);
SETUPDISTTABLE(1,2);
@ -808,7 +899,7 @@ static boolean K_ForcedSPB(const player_t *player, itemroulette_t *const roulett
return false;
}
if (specialStage.active == true)
if (specialstageinfo.valid == true)
{
return false;
}
@ -904,7 +995,7 @@ static void K_InitRoulette(itemroulette_t *const roulette)
roulette->exiting++;
}
if (specialStage.active == true)
if (specialstageinfo.valid == true)
{
UINT32 dis = K_UndoMapScaling(players[i].distancetofinish);
if (dis < roulette->secondDist)
@ -926,7 +1017,7 @@ static void K_InitRoulette(itemroulette_t *const roulette)
}
}
if (specialStage.active == true)
if (specialstageinfo.valid == true)
{
roulette->firstDist = K_UndoMapScaling(K_GetSpecialUFODistance());
}
@ -1113,8 +1204,19 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
}
// SPECIAL CASE No. 2:
// Use a special, pre-determined item reel for Time Attack / Free Play
if (bossinfo.boss == true)
// Use a special, pre-determined item reel for Time Attack / Free Play / End of Sealed Stars
if (specialstageinfo.valid)
{
if (K_GetPossibleSpecialTarget() == NULL)
{
for (i = 0; K_KartItemReelSpecialEnd[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, K_KartItemReelSpecialEnd[i]);
}
return;
}
}
else if (gametyperules & GTR_BOSS)
{
for (i = 0; K_KartItemReelBoss[i] != KITEM_NONE; i++)
{
@ -1125,25 +1227,17 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet
}
else if (K_TimeAttackRules() == true)
{
switch (gametype)
kartitems_t *presetlist = K_KartItemReelTimeAttack;
// If the objective is not to go fast, it's to cause serious damage.
if (gametyperules & GTR_CAPSULES)
{
case GT_RACE:
default:
{
for (i = 0; K_KartItemReelTimeAttack[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, K_KartItemReelTimeAttack[i]);
}
break;
}
case GT_BATTLE:
{
for (i = 0; K_KartItemReelBreakTheCapsules[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, K_KartItemReelBreakTheCapsules[i]);
}
break;
}
presetlist = K_KartItemReelBreakTheCapsules;
}
for (i = 0; presetlist[i] != KITEM_NONE; i++)
{
K_PushToRouletteItemList(roulette, presetlist[i]);
}
return;

View file

@ -22,7 +22,7 @@
#include "k_waypoint.h"
#include "k_objects.h"
struct specialStage specialStage;
struct specialstageinfo specialstageinfo;
/*--------------------------------------------------
void K_ResetSpecialStage(void)
@ -31,7 +31,8 @@ struct specialStage specialStage;
--------------------------------------------------*/
void K_ResetSpecialStage(void)
{
memset(&specialStage, 0, sizeof(struct specialStage));
memset(&specialstageinfo, 0, sizeof(struct specialstageinfo));
specialstageinfo.beamDist = UINT32_MAX;
}
/*--------------------------------------------------
@ -41,34 +42,15 @@ void K_ResetSpecialStage(void)
--------------------------------------------------*/
void K_InitSpecialStage(void)
{
INT32 i;
specialStage.beamDist = UINT32_MAX; // TODO: make proper value
P_SetTarget(&specialStage.ufo, Obj_CreateSpecialUFO());
for (i = 0; i < MAXPLAYERS; i++)
if ((gametyperules & (GTR_CATCHER|GTR_CIRCUIT)) != (GTR_CATCHER|GTR_CIRCUIT))
{
player_t *player = NULL;
if (playeringame[i] == false)
{
continue;
}
player = &players[i];
if (player->spectator == true)
{
continue;
}
if (player->mo == NULL || P_MobjWasRemoved(player->mo) == true)
{
continue;
}
// Rolling start? lol
P_InstaThrust(player->mo, player->mo->angle, K_GetKartSpeed(player, false, false));
return;
}
specialstageinfo.valid = true;
specialstageinfo.beamDist = UINT32_MAX; // TODO: make proper value
P_SetTarget(&specialstageinfo.ufo, Obj_CreateSpecialUFO());
}
/*--------------------------------------------------
@ -88,15 +70,15 @@ static void K_MoveExitBeam(void)
moveDist = (8 * mapobjectscale) / FRACUNIT;
if (specialStage.beamDist <= moveDist)
if (specialstageinfo.beamDist <= moveDist)
{
specialStage.beamDist = 0;
specialstageinfo.beamDist = 0;
// TODO: Fail Special Stage
}
else
{
specialStage.beamDist -= moveDist;
specialstageinfo.beamDist -= moveDist;
}
// Find players who are now outside of the level.
@ -118,7 +100,7 @@ static void K_MoveExitBeam(void)
continue;
}
if (player->distancetofinish > specialStage.beamDist)
if (player->distancetofinish > specialstageinfo.beamDist)
{
P_DoTimeOver(player);
}
@ -132,10 +114,30 @@ static void K_MoveExitBeam(void)
--------------------------------------------------*/
void K_TickSpecialStage(void)
{
if (specialStage.active == false)
if (specialstageinfo.valid == false)
{
return;
}
if (P_MobjWasRemoved(specialstageinfo.ufo))
{
P_SetTarget(&specialstageinfo.ufo, NULL);
}
K_MoveExitBeam();
}
mobj_t *K_GetPossibleSpecialTarget(void)
{
if (specialstageinfo.valid == false)
return NULL;
if (specialstageinfo.ufo == NULL
|| P_MobjWasRemoved(specialstageinfo.ufo))
return NULL;
if (specialstageinfo.ufo->health <= 1) //UFOEmeraldChase(specialstageinfo.ufo)
return NULL;
return specialstageinfo.ufo;
}

View file

@ -20,14 +20,13 @@
extern "C" {
#endif
extern struct specialStage
extern struct specialstageinfo
{
boolean active; ///< If true, then we are in a special stage
boolean encore; ///< Copy of encore, just to make sure you can't cheat it with cvars
boolean valid; ///< If true, then data in this struct is valid
UINT32 beamDist; ///< Where the exit beam is.
mobj_t *ufo; ///< The Chaos Emerald capsule.
} specialStage;
} specialstageinfo;
/*--------------------------------------------------
void K_ResetSpecialStage(void);
@ -55,6 +54,18 @@ void K_InitSpecialStage(void);
void K_TickSpecialStage(void);
/*--------------------------------------------------
mobj_t *K_GetPossibleSpecialTarget(void)
Gets the global special stage target if valid
(for Jawz, tether, etc)
Return:-
Target or NULL
--------------------------------------------------*/
mobj_t *K_GetPossibleSpecialTarget(void);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -1949,6 +1949,7 @@ static waypoint_t *K_MakeWaypoint(mobj_t *const mobj)
madewaypoint = &waypointheap[numwaypoints];
numwaypoints++;
madewaypoint->mobj = NULL;
P_SetTarget(&madewaypoint->mobj, mobj);
// Don't allow a waypoint that has its next ID set to itself to work

View file

@ -2973,15 +2973,16 @@ static int lib_gAddGametype(lua_State *L)
const char *k;
lua_Integer i;
gametype_t *newgametype = NULL;
const char *gtname = NULL;
const char *gtconst = NULL;
INT16 newgtidx = 0;
UINT32 newgtrules = 0;
UINT32 newgttol = 0;
INT32 newgtpointlimit = 0;
INT32 newgttimelimit = 0;
INT16 newgtrankingstype = -1;
int newgtinttype = 0;
UINT8 newgtinttype = 0;
INT16 j;
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 1); // Clear out all other possible arguments, leaving only the first one.
@ -2990,8 +2991,10 @@ static int lib_gAddGametype(lua_State *L)
return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
// Ran out of gametype slots
if (gametypecount == NUMGAMETYPEFREESLOTS)
return luaL_error(L, "Ran out of free gametype slots!");
if (numgametypes == GT_LASTFREESLOT)
{
I_Error("Out of Gametype Freeslots while allocating \"%s\"\nLoad less addons to fix this.", gtname);
}
#define FIELDERROR(f, e) luaL_error(L, "bad value for " LUA_QL(f) " in table passed to " LUA_QL("G_AddGametype") " (%s)", e);
#define TYPEERROR(f, t) FIELDERROR(f, va("%s expected, got %s", lua_typename(L, t), luaL_typename(L, -1)))
@ -3024,19 +3027,15 @@ static int lib_gAddGametype(lua_State *L)
if (!lua_isnumber(L, 3))
TYPEERROR("typeoflevel", LUA_TNUMBER)
newgttol = (UINT32)lua_tointeger(L, 3);
} else if (i == 5 || (k && fasticmp(k, "rankingtype"))) {
if (!lua_isnumber(L, 3))
TYPEERROR("rankingtype", LUA_TNUMBER)
newgtrankingstype = (INT16)lua_tointeger(L, 3);
} else if (i == 6 || (k && fasticmp(k, "intermissiontype"))) {
} else if (i == 5 || (k && fasticmp(k, "intermissiontype"))) {
if (!lua_isnumber(L, 3))
TYPEERROR("intermissiontype", LUA_TNUMBER)
newgtinttype = (int)lua_tointeger(L, 3);
} else if (i == 7 || (k && fasticmp(k, "defaultpointlimit"))) {
} else if (i == 6 || (k && fasticmp(k, "defaultpointlimit"))) {
if (!lua_isnumber(L, 3))
TYPEERROR("defaultpointlimit", LUA_TNUMBER)
newgtpointlimit = (INT32)lua_tointeger(L, 3);
} else if (i == 8 || (k && fasticmp(k, "defaulttimelimit"))) {
} else if (i == 7 || (k && fasticmp(k, "defaulttimelimit"))) {
if (!lua_isnumber(L, 3))
TYPEERROR("defaulttimelimit", LUA_TNUMBER)
newgttimelimit = (INT32)lua_tointeger(L, 3);
@ -3047,38 +3046,44 @@ static int lib_gAddGametype(lua_State *L)
#undef FIELDERROR
#undef TYPEERROR
if (gtname == NULL)
return luaL_error(L, "Custom gametype must have a name");
if (strlen(gtname) >= MAXGAMETYPELENGTH)
return luaL_error(L, "Custom gametype \"%s\"'s name must be %d long at most", gtname, MAXGAMETYPELENGTH-1);
for (j = 0; j < numgametypes; j++)
if (!strcmp(gtname, gametypes[j]->name))
break;
if (j < numgametypes)
return luaL_error(L, "Custom gametype \"%s\"'s name is already in use", gtname);
// pop gametype table
lua_pop(L, 1);
// Set defaults
if (gtname == NULL)
gtname = Z_StrDup("Unnamed gametype");
// Add the new gametype
newgtidx = G_AddGametype(newgtrules);
G_AddGametypeTOL(newgtidx, newgttol);
newgametype = Z_Calloc(sizeof (gametype_t), PU_STATIC, NULL);
if (!newgametype)
{
I_Error("Out of memory allocating gametype \"%s\"", gtname);
}
// Not covered by G_AddGametype alone.
if (newgtrankingstype == -1)
newgtrankingstype = newgtidx;
gametyperankings[newgtidx] = newgtrankingstype;
intermissiontypes[newgtidx] = newgtinttype;
pointlimits[newgtidx] = newgtpointlimit;
timelimits[newgtidx] = newgttimelimit;
// Write the new gametype name.
Gametype_Names[newgtidx] = gtname;
// Write the constant name.
if (gtconst == NULL)
gtconst = gtname;
G_AddGametypeConstant(newgtidx, gtconst);
// Update gametype_cons_t accordingly.
G_UpdateGametypeSelections();
newgametype->name = gtname;
newgametype->rules = newgtrules;
newgametype->constant = G_PrepareGametypeConstant(gtconst);
newgametype->tol = newgttol;
newgametype->intermission = newgtinttype;
newgametype->pointlimit = newgtpointlimit;
newgametype->timelimit = newgttimelimit;
gametypes[numgametypes++] = newgametype;
// done
CONS_Printf("Added gametype %s\n", Gametype_Names[newgtidx]);
CONS_Printf("Added gametype %s\n", gtname);
return 0;
}
@ -3286,15 +3291,6 @@ static int lib_gExitLevel(lua_State *L)
return 0;
}
static int lib_gIsSpecialStage(lua_State *L)
{
INT32 mapnum = luaL_optinteger(L, 1, gamemap);
//HUDSAFE
INLEVEL
lua_pushboolean(L, G_IsSpecialStage(mapnum));
return 1;
}
static int lib_gGametypeUsesLives(lua_State *L)
{
//HUDSAFE
@ -4103,7 +4099,6 @@ static luaL_Reg lib[] = {
{"G_DoReborn",lib_gDoReborn},
{"G_SetCustomExitVars",lib_gSetCustomExitVars},
{"G_ExitLevel",lib_gExitLevel},
{"G_IsSpecialStage",lib_gIsSpecialStage},
{"G_GametypeUsesLives",lib_gGametypeUsesLives},
{"G_GametypeHasTeams",lib_gGametypeHasTeams},
{"G_GametypeHasSpectators",lib_gGametypeHasSpectators},

View file

@ -162,9 +162,6 @@ int LUA_PushGlobals(lua_State *L, const char *word)
} else if (fastcmp(word,"maptol")) {
lua_pushinteger(L, maptol);
return 1;
} else if (fastcmp(word,"circuitmap")) {
lua_pushboolean(L, circuitmap);
return 1;
} else if (fastcmp(word,"stoppedclock")) {
lua_pushboolean(L, stoppedclock);
return 1;

View file

@ -97,21 +97,27 @@ void M_PopulateChallengeGrid(void)
{
// Getting the number of 2-highs you can fit into two adjacent columns.
UINT8 majorpad = (CHALLENGEGRIDHEIGHT/2);
majorpad = (nummajorunlocks+1)/majorpad;
numempty = nummajorunlocks%majorpad;
majorpad = (nummajorunlocks+(majorpad-1))/majorpad;
gamedata->challengegridwidth = majorpad*2;
numempty *= 4;
#if (CHALLENGEGRIDHEIGHT % 2)
// One empty per column.
numempty = gamedata->challengegridwidth;
// One extra empty per column.
numempty += gamedata->challengegridwidth;
#endif
//CONS_Printf("%d major unlocks means width of %d, numempty of %d\n", nummajorunlocks, gamedata->challengegridwidth, numempty);
}
if (numunlocks > numempty)
{
// Getting the number of extra columns to store normal unlocks
gamedata->challengegridwidth += ((numunlocks - numempty) + (CHALLENGEGRIDHEIGHT-1))/CHALLENGEGRIDHEIGHT;
UINT16 temp = ((numunlocks - numempty) + (CHALLENGEGRIDHEIGHT-1))/CHALLENGEGRIDHEIGHT;
gamedata->challengegridwidth += temp;
majorcompact = 1;
//CONS_Printf("%d normal unlocks means %d extra entries, additional width of %d\n", numunlocks, (numunlocks - numempty), temp);
}
else if (challengegridloops)
{
@ -570,7 +576,10 @@ static char *M_BuildConditionTitle(UINT16 map)
{
char *title, *ref;
if (M_MapLocked(map+1))
if (((mapheaderinfo[map]->menuflags & LF2_FINISHNEEDED)
// the following is intentionally not MV_BEATEN, just in case the title is for "Finish a round on X"
&& !(mapheaderinfo[map]->mapvisited & MV_VISITED))
|| M_MapLocked(map+1))
return Z_StrDup("???");
title = ref = G_BuildMapTitle(map+1);
@ -629,7 +638,7 @@ static const char *M_GetConditionString(condition_t *cn)
title = BUILDCONDITIONTITLE(cn->requirement);
work = va("%s %s%s",
(cn->type == UC_MAPVISITED) ? "Visit" : "Beat",
(cn->type == UC_MAPVISITED) ? "Visit" : "Finish a round on",
title,
(cn->type == UC_MAPENCORE) ? " in Encore Mode" : "");
Z_Free(title);

View file

@ -123,7 +123,8 @@ typedef enum
// Menu restrictions
SECRET_TIMEATTACK, // Permit Time attack
SECRET_BREAKTHECAPSULES, // Permit SP Capsules
SECRET_BREAKTHECAPSULES, // Permit SP Capsule attack
SECRET_SPECIALATTACK, // Permit Special attack (You're blue now!)
SECRET_SOUNDTEST, // Permit Sound Test
SECRET_ALTTITLE, // Permit alternate titlescreen

View file

@ -10,4 +10,6 @@ target_sources(SRB2SDL2 PRIVATE
duel-bomb.c
broly.c
ufo.c
monitor.c
item-spot.c
)

View file

@ -34,14 +34,16 @@ Obj_SpawnBrolyKi
( mobj_t * source,
tic_t duration)
{
mobj_t *x = P_SpawnMobjFromMobj(
source, 0, 0, 0, MT_BROLY);
mobj_t *x;
if (duration == 0)
if (duration <= 0)
{
return x;
return NULL;
}
x = P_SpawnMobjFromMobj(
source, 0, 0, 0, MT_BROLY);
// Shrink into center of source object.
x->z = (source->z + source->height / 2);
@ -61,12 +63,20 @@ Obj_SpawnBrolyKi
return x;
}
void
boolean
Obj_BrolyKiThink (mobj_t *x)
{
if (broly_duration(x) <= 0)
{
P_RemoveMobj(x);
return false;
}
const fixed_t
t = get_unit_linear(x),
n = Easing_OutSine(t, 0, broly_maxscale(x));
P_InstaScale(x, n);
return true;
}

35
src/objects/item-spot.c Normal file
View file

@ -0,0 +1,35 @@
#include "../doomdef.h"
#include "../m_fixed.h"
#include "../k_objects.h"
#include "../k_battle.h"
#include "../p_tick.h"
#include "../p_local.h"
#define spot_monitor(o) ((o)->target)
#define spot_cool(o) ((o)->threshold)
boolean
Obj_ItemSpotIsAvailable (const mobj_t *spot)
{
return P_MobjWasRemoved(spot_monitor(spot)) &&
(leveltime - spot_cool(spot)) > BATTLE_SPAWN_INTERVAL;
}
void
Obj_ItemSpotAssignMonitor
( mobj_t * spot,
mobj_t * monitor)
{
P_SetTarget(&spot_monitor(spot), monitor);
Obj_MonitorSetItemSpot(monitor, spot);
}
void
Obj_ItemSpotUpdate (mobj_t *spot)
{
if (P_MobjWasRemoved(spot_monitor(spot)) ||
spot_monitor(spot)->health <= 0)
{
spot_cool(spot) = leveltime;
}
}

705
src/objects/monitor.c Normal file
View file

@ -0,0 +1,705 @@
#include "../doomdef.h"
#include "../doomstat.h"
#include "../info.h"
#include "../k_objects.h"
#include "../p_local.h"
#include "../r_state.h"
#include "../k_kart.h"
#include "../k_battle.h"
#include "../m_random.h"
#include "../r_main.h"
#define FINE90 (FINEANGLES/4)
#define FINE180 (FINEANGLES/2)
#define TRUETAN(n) FINETANGENT(FINE90 + (n)) // bruh
#define HEALTHFACTOR (FRACUNIT/4) // Always takes at most, 4 hits.
#define MONITOR_PART_DEFINE(dispoffset, nsides, ...) \
{dispoffset, nsides, (statenum_t[]){__VA_ARGS__, 0}}
static const struct monitor_part_config {
INT32 dispoffset;
UINT8 nsides;
statenum_t * states;
} monitor_parts[] = {
MONITOR_PART_DEFINE (0, 3,
S_MONITOR_SCREEN1A,
S_ITEMICON,
S_MONITOR_CRACKB,
S_MONITOR_CRACKA),
MONITOR_PART_DEFINE (-5, 5, S_MONITOR_STAND),
};
#define monitor_spot(o) ((o)->target)
#define monitor_rngseed(o) ((o)->movedir)
#define monitor_itemcount(o) ((o)->movecount)
#define monitor_spawntic(o) ((o)->reactiontime)
#define monitor_emerald(o) ((o)->extravalue1)
#define monitor_damage(o) ((o)->extravalue2)
#define monitor_rammingspeed(o) ((o)->movefactor)
static inline UINT8
get_monitor_itemcount (const mobj_t *monitor)
{
// protects against divide by zero
return max(monitor_itemcount(monitor), 1);
}
#define part_monitor(o) ((o)->target)
#define part_type(o) ((o)->extravalue1)
#define part_index(o) ((o)->extravalue2)
#define part_theta(o) ((o)->movedir)
#define shard_can_roll(o) ((o)->extravalue1)
static const sprcache_t * get_state_sprcache (statenum_t);
static const sprcache_t *
get_sprcache
( spritenum_t sprite,
UINT8 frame)
{
const spritedef_t *sprdef = &sprites[sprite];
if (frame < sprdef->numframes)
{
size_t lump = sprdef->spriteframes[frame].lumpid[0];
return &spritecachedinfo[lump];
}
else
{
return get_state_sprcache(S_UNKNOWN);
}
}
static const sprcache_t *
get_state_sprcache (statenum_t statenum)
{
return get_sprcache(states[statenum].sprite,
states[statenum].frame & FF_FRAMEMASK);
}
static inline fixed_t
get_inradius
( fixed_t length,
INT32 nsides)
{
return FixedDiv(length, 2 * TRUETAN(FINE180 / nsides));
}
static inline void
center_item_sprite
( mobj_t * part,
fixed_t scale)
{
part->spriteyoffset = 25*FRACUNIT;
part->spritexscale = scale;
part->spriteyscale = scale;
}
static mobj_t *
spawn_part
( mobj_t * monitor,
statenum_t state)
{
mobj_t *part = P_SpawnMobjFromMobj(
monitor, 0, 0, 0, MT_MONITOR_PART);
P_SetMobjState(part, state);
P_SetTarget(&part_monitor(part), monitor);
part_type(part) = state;
switch (state)
{
case S_ITEMICON:
// The first frame of the monitor is TV static so
// this should be invisible on the first frame.
part->renderflags |= RF_DONTDRAW;
break;
default:
break;
}
return part;
}
static void
spawn_part_side
( mobj_t * monitor,
fixed_t rad,
fixed_t ang,
const struct monitor_part_config * p,
size_t side)
{
INT32 i = 0;
while (p->states[i])
{
mobj_t *part = spawn_part(monitor, p->states[i]);
part->radius = rad;
part_theta(part) = ang;
// add one point for each layer (back to front order)
part->dispoffset = p->dispoffset + i;
part_index(part) = side;
i++;
}
}
static void
spawn_monitor_parts
( mobj_t * monitor,
const struct monitor_part_config *p)
{
const sprcache_t *info = get_state_sprcache(p->states[0]);
const fixed_t width = FixedMul(monitor->scale, info->width);
const fixed_t rad = get_inradius(width, p->nsides);
const fixed_t angle_factor = ANGLE_MAX / p->nsides;
INT32 i;
angle_t ang = 0;
for (i = 0; i < p->nsides; ++i)
{
spawn_part_side(monitor, rad, ang, p, i);
ang += angle_factor;
}
}
static inline boolean
can_shard_state_roll (statenum_t state)
{
switch (state)
{
case S_MONITOR_BIG_SHARD:
case S_MONITOR_SMALL_SHARD:
return true;
default:
return false;
}
}
static void
spawn_shard
( mobj_t * part,
statenum_t state)
{
mobj_t *monitor = part_monitor(part);
// These divisions and multiplications are done on the
// offsets to give bigger increments of randomness.
const fixed_t half = FixedDiv(
monitor->height, monitor->scale) / 2;
const UINT16 rad = (monitor->radius / monitor->scale) / 4;
const UINT16 tall = (half / FRACUNIT) / 4;
mobj_t *p = P_SpawnMobjFromMobj(monitor,
P_RandomRange(PR_ITEM_DEBRIS, -(rad), rad) * 8 * FRACUNIT,
P_RandomRange(PR_ITEM_DEBRIS, -(rad), rad) * 8 * FRACUNIT,
(half / 4) + P_RandomKey(PR_ITEM_DEBRIS, tall + 1) * 4 * FRACUNIT,
MT_MONITOR_SHARD);
angle_t th = (part->angle + ANGLE_90);
th -= P_RandomKey(PR_ITEM_DEBRIS, ANGLE_45) - ANGLE_22h;
p->hitlag = 0;
P_Thrust(p, th, 6 * p->scale + monitor_rammingspeed(monitor));
p->momz = P_RandomRange(PR_ITEM_DEBRIS, 3, 10) * p->scale;
P_SetMobjState(p, state);
shard_can_roll(p) = can_shard_state_roll(state);
if (shard_can_roll(p))
{
p->rollangle = P_Random(PR_ITEM_DEBRIS);
}
if (P_RandomChance(PR_ITEM_DEBRIS, FRACUNIT/2))
{
p->renderflags |= RF_DONTDRAW;
}
}
static void
spawn_debris (mobj_t *part)
{
const mobj_t *monitor = part_monitor(part);
fixed_t i;
for (i = monitor->health;
i <= FRACUNIT; i += HEALTHFACTOR/2)
{
spawn_shard(part, S_MONITOR_BIG_SHARD);
spawn_shard(part, S_MONITOR_SMALL_SHARD);
spawn_shard(part, S_MONITOR_TWINKLE);
}
}
static void
spawn_monitor_explosion (mobj_t *monitor)
{
mobj_t *smoldering = P_SpawnMobjFromMobj(monitor, 0, 0, 0, MT_SMOLDERING);
UINT8 i;
// Note that a Broly Ki is purposefully not spawned. This
// is to reduce visual clutter since these monitors would
// probably get popped a lot.
K_MineFlashScreen(monitor);
P_SetScale(smoldering, (smoldering->destscale /= 3));
smoldering->tics = TICRATE*3;
for (i = 0; i < 8; ++i)
{
mobj_t *x = P_SpawnMobjFromMobj(monitor, 0, 0, 0, MT_BOOMEXPLODE);
x->hitlag = 0;
x->color = SKINCOLOR_WHITE;
x->momx = P_RandomRange(PR_EXPLOSION, -5, 5) * monitor->scale,
x->momy = P_RandomRange(PR_EXPLOSION, -5, 5) * monitor->scale,
x->momz = P_RandomRange(PR_EXPLOSION, 0, 6) * monitor->scale * P_MobjFlip(monitor);
P_SetScale(x, (x->destscale *= 3));
}
}
static void
kill_monitor_part (mobj_t *part)
{
const statenum_t statenum = part_type(part);
switch (statenum)
{
case S_ITEMICON:
P_RemoveMobj(part);
return;
case S_MONITOR_STAND:
part->momx = 0;
part->momy = 0;
break;
case S_MONITOR_SCREEN1A:
spawn_debris(part);
P_SetMobjState(part, S_MONITOR_SCREEN1B);
/*FALLTHRU*/
default:
/* To be clear, momx/y do not need to set because
those fields are set every tic to offset each
part. */
part->momz = (part->height / 8) * P_MobjFlip(part);
}
part->fuse = TICRATE;
part->flags &= ~(MF_NOGRAVITY);
}
static inline UINT32
restore_item_rng (UINT32 seed)
{
const UINT32 oldseed = P_GetRandSeed(PR_ITEM_ROULETTE);
P_SetRandSeedNet(PR_ITEM_ROULETTE,
P_GetInitSeed(PR_ITEM_ROULETTE), seed);
return oldseed;
}
static inline SINT8
get_item_result (void)
{
return K_GetTotallyRandomResult(0);
}
static SINT8
get_cycle_result
( const mobj_t * monitor,
size_t cycle)
{
const size_t rem = cycle %
get_monitor_itemcount(monitor);
SINT8 result;
size_t i;
const UINT32 oldseed = restore_item_rng(
monitor_rngseed(monitor));
for (i = 0; i <= rem; ++i)
{
result = get_item_result();
}
restore_item_rng(oldseed);
return result;
}
static inline tic_t
get_age (const mobj_t *monitor)
{
return (leveltime - monitor_spawntic(monitor));
}
static inline boolean
is_flickering (const mobj_t *part)
{
const mobj_t *monitor = part_monitor(part);
return monitor->fuse > 0 && monitor->fuse <= TICRATE;
}
static void
flicker
( mobj_t * part,
UINT8 interval)
{
const tic_t age = get_age(part_monitor(part));
if (age % interval)
{
part->renderflags |= RF_DONTDRAW;
}
else
{
part->renderflags &= ~(RF_DONTDRAW);
}
}
static void
project_icon (mobj_t *part)
{
const mobj_t *monitor = part_monitor(part);
const tic_t age = get_age(monitor);
// Item displayed on monitor cycles every N tics
if (age % 64 == 0)
{
const SINT8 result = get_cycle_result(monitor,
part_index(part) + (age / 64));
K_UpdateMobjItemOverlay(part,
K_ItemResultToType(result),
K_ItemResultToAmount(result));
center_item_sprite(part, 5*FRACUNIT/4);
}
flicker(part, is_flickering(part) ? 4 : 2);
}
static void
translate (mobj_t *part)
{
const angle_t ang = part_theta(part) +
part_monitor(part)->angle;
part->angle = (ang - ANGLE_90);
// Because of MF_NOCLIPTHING, no friction is applied.
// This object is teleported back to the monitor every
// tic so its position is in total only ever translated
// by this much.
part->momx = P_ReturnThrustX(NULL, ang, part->radius);
part->momy = P_ReturnThrustY(NULL, ang, part->radius);
}
static inline fixed_t
get_damage_multiplier (const mobj_t *monitor)
{
return FixedDiv(monitor_damage(monitor), HEALTHFACTOR);
}
static inline boolean
has_state
( const mobj_t * mobj,
statenum_t state)
{
return mobj->hitlag == 0 &&
(size_t)(mobj->state - states) == (size_t)state;
}
static mobj_t *
adjust_monitor_drop
( mobj_t * monitor,
mobj_t * drop)
{
P_InstaThrust(drop, drop->angle, 4*mapobjectscale);
drop->momz *= 8;
K_FlipFromObject(drop, monitor);
return drop;
}
void
Obj_MonitorSpawnParts (mobj_t *monitor)
{
const size_t nparts =
sizeof monitor_parts / sizeof *monitor_parts;
size_t i;
P_SetScale(monitor, (monitor->destscale *= 2));
monitor_itemcount(monitor) = 0;
monitor_rngseed(monitor) = P_GetRandSeed(PR_ITEM_ROULETTE);
monitor_spawntic(monitor) = leveltime;
monitor_emerald(monitor) = 0;
for (i = 0; i < nparts; ++i)
{
spawn_monitor_parts(monitor, &monitor_parts[i]);
}
}
mobj_t *
Obj_SpawnMonitor
( mobj_t * origin,
UINT8 numItemTypes,
UINT8 emerald)
{
mobj_t *monitor = P_SpawnMobj(origin->x, origin->y,
origin->z, MT_MONITOR);
monitor->angle = P_Random(PR_DECORATION);
monitor_itemcount(monitor) = numItemTypes;
monitor_emerald(monitor) = emerald;
monitor->color = K_GetChaosEmeraldColor(emerald);
return monitor;
}
void
Obj_MonitorPartThink (mobj_t *part)
{
const statenum_t statenum = part_type(part);
mobj_t *monitor = part_monitor(part);
if (part->fuse > 0)
{
return;
}
if (P_MobjWasRemoved(monitor))
{
P_RemoveMobj(part);
return;
}
if (has_state(monitor, monitor->info->deathstate))
{
kill_monitor_part(part);
return;
}
if (is_flickering(part))
{
flicker(part, 2);
}
if (monitor->hitlag)
{
const fixed_t shake = FixedMul(
2 * get_damage_multiplier(monitor),
monitor->radius / 8);
part->sprxoff = P_AltFlip(shake, 2);
part->spryoff = P_AltFlip(shake, 4);
}
else
{
part->sprxoff = 0;
part->spryoff = 0;
}
switch (statenum)
{
case S_MONITOR_SCREEN1A:
if (has_state(monitor, monitor->info->painstate))
{
spawn_debris(part);
}
break;
case S_MONITOR_CRACKA:
case S_MONITOR_CRACKB:
if (monitor->health < monitor->info->spawnhealth)
{
part->sprite = SPR_IMON; // initially SPR_NULL
part->frame = part->state->frame +
(monitor->health / HEALTHFACTOR);
}
break;
case S_ITEMICON:
project_icon(part);
break;
default:
break;
}
P_MoveOrigin(part, monitor->x, monitor->y, monitor->z);
translate(part);
}
fixed_t
Obj_MonitorGetDamage
( mobj_t * monitor,
mobj_t * inflictor,
UINT8 damagetype)
{
fixed_t damage;
switch (damagetype & DMG_TYPEMASK)
{
case DMG_VOLTAGE:
if (monitor->health < HEALTHFACTOR)
{
return HEALTHFACTOR;
}
else
{
// always reduce to final damage state
return (monitor->health - HEALTHFACTOR) + 1;
}
}
if (inflictor == NULL)
{
return HEALTHFACTOR;
}
if (inflictor->player)
{
const fixed_t weight =
K_GetMobjWeight(inflictor, monitor);
// HEALTHFACTOR is the minimum damage that can be
// dealt but player's weight (and speed) can buff the hit.
damage = HEALTHFACTOR +
(FixedMul(weight, HEALTHFACTOR) / 9);
if (inflictor->scale > mapobjectscale)
{
damage = P_ScaleFromMap(damage, inflictor->scale);
}
}
else
{
damage = FRACUNIT; // kill instantly
}
return damage;
}
void
Obj_MonitorOnDamage
( mobj_t * monitor,
mobj_t * inflictor,
INT32 damage)
{
monitor->fuse = BATTLE_DESPAWN_TIME;
monitor_damage(monitor) = damage;
monitor_rammingspeed(monitor) = inflictor
? FixedDiv(FixedHypot(inflictor->momx, inflictor->momy), 4 * inflictor->radius) : 0;
monitor->hitlag =
6 * get_damage_multiplier(monitor) / FRACUNIT;
}
void
Obj_MonitorOnDeath (mobj_t *monitor)
{
const UINT8 itemcount = get_monitor_itemcount(monitor);
const angle_t ang = ANGLE_MAX / itemcount;
const SINT8 flip = P_MobjFlip(monitor);
INT32 i;
UINT32 sharedseed = restore_item_rng(
monitor_rngseed(monitor));
for (i = 0; i < itemcount; ++i)
{
const SINT8 result = get_item_result();
const UINT32 localseed = restore_item_rng(sharedseed);
adjust_monitor_drop(monitor,
K_CreatePaperItem(
monitor->x, monitor->y, monitor->z + (128 * mapobjectscale * flip),
i * ang, flip,
K_ItemResultToType(result),
K_ItemResultToAmount(result)));
// K_CreatePaperItem may advance RNG, so update our
// copy of the seed afterward
sharedseed = restore_item_rng(localseed);
}
restore_item_rng(sharedseed);
if (monitor_emerald(monitor) != 0)
{
adjust_monitor_drop(monitor,
K_SpawnChaosEmerald(monitor->x, monitor->y, monitor->z + (128 * mapobjectscale * flip),
ang, flip, monitor_emerald(monitor)));
}
spawn_monitor_explosion(monitor);
// There is hitlag from being damaged, so remove
// tangibility RIGHT NOW.
monitor->flags &= ~(MF_SOLID);
if (!P_MobjWasRemoved(monitor_spot(monitor)))
{
Obj_ItemSpotUpdate(monitor_spot(monitor));
}
}
void
Obj_MonitorShardThink (mobj_t *shard)
{
if (shard_can_roll(shard))
{
shard->rollangle += ANGLE_45;
}
shard->renderflags ^= RF_DONTDRAW;
}
UINT32
Obj_MonitorGetEmerald (const mobj_t *monitor)
{
return monitor_emerald(monitor);
}
void
Obj_MonitorSetItemSpot
( mobj_t * monitor,
mobj_t * spot)
{
P_SetTarget(&monitor_spot(monitor), spot);
}

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