Compare commits

...
Author SHA1 Message Date
niffiwan 7e6eb25492 triple braces clang? really? 2018-08-11 22:37:57 +10:00
niffiwan 9de5d8d011 add asteroid editor to qtfred 2018-08-11 20:49:55 +10:00
Fabian Woltermann 62d178d896 Merge pull request #1818 from asarium/feature/loopingMovies
Add support for looping movies
2018-08-10 20:46:36 +02:00
Fabian Woltermann f8b210b659 Merge pull request #1819 from asarium/feature/vec3dOperators
Add C++ operators for vector math operations
2018-08-10 20:45:45 +02:00
Fabian Woltermann d3e6ee81e1 Merge pull request #1822 from asarium/fix/fredFlags
Terminate FRED/qtFRED when command line demands it
2018-08-10 20:44:56 +02:00
asarium 66f95312ae Terminate FRED/qtFRED when command line demands it
This was reported by ngld who experienced issues with Knossos since FRED
did not terminate properly when asked to provide the command line flags.
This should fix that.
2018-08-01 09:20:18 +02:00
asarium ead93f3175 Add C++ operators for vector math operations
This makes it a bit easier to use the vector types since they are now
usable with the standard C++ operators.
2018-07-30 23:38:51 +02:00
asarium 48b1e3f289 Add support for looping movies
This adds a playback property for making the movie loop to the start
when the end is reached. The way this is done is by simply keeping the
decoder running even if the end of the movie is reached. Then the movie
is reset to the beginning and decoding continues. The player detects
this reset by examining the frame times which keeps the playback
constant.

This also includes support for this in the movie scripting API.
2018-07-30 18:10:48 +02:00
26 changed files with 1648 additions and 104 deletions

No files matched your search

+6 -1
View File
@@ -73,6 +73,11 @@ struct SubtitleFrame {
};
typedef std::unique_ptr<SubtitleFrame> SubtitleFramePtr;
struct PlaybackProperties {
bool with_audio = true;
bool looping = false;
};
/**
* @brief Abstract class for decoding a video or audio stream
*
@@ -106,7 +111,7 @@ class Decoder {
* @param fileName The name of the file that should be opened
* @return @c true if the initialization was successfull, @c false otherwise
*/
virtual bool initialize(const SCP_string& fileName) = 0;
virtual bool initialize(const SCP_string& fileName, const PlaybackProperties& properties) = 0;
/**
* @brief Returns the properties of the video
+3
View File
@@ -223,5 +223,8 @@ void AudioDecoder::finishDecoding() {
// Push the last bits of audio data into the queue
flushAudioBuffer();
}
void AudioDecoder::flushBuffers() {
avcodec_flush_buffers(m_status->audioCodecCtx);
}
}
}
+2
View File
@@ -30,6 +30,8 @@ class AudioDecoder: public FFMPEGStreamDecoder<AudioFrame> {
void decodePacket(AVPacket* packet) override;
void finishDecoding() override;
void flushBuffers() override;
};
}
}
+105 -71
View File
@@ -28,6 +28,9 @@ class AVPacketScope
: _packet(av_packet) {
}
AVPacketScope(const AVPacketScope&) = delete;
AVPacketScope& operator=(const AVPacketScope&) = delete;
~AVPacketScope() {
av_packet_unref(_packet);
}
@@ -214,8 +217,14 @@ std::unique_ptr<InputStream> openStream(const SCP_string& name) {
}
std::unique_ptr<DecoderStatus> initializeStatus(std::unique_ptr<InputStream>& stream,
std::unique_ptr<InputStream>& subt)
std::unique_ptr<InputStream>& subt,
const PlaybackProperties& properties)
{
if (subt && properties.looping) {
mprintf(("FFmpeg: External subtitles and looping movies are not supported!\n"));
return nullptr;
}
std::unique_ptr<DecoderStatus> status(new DecoderStatus());
auto ctx = stream->m_ctx->ctx();
@@ -233,14 +242,18 @@ std::unique_ptr<DecoderStatus> initializeStatus(std::unique_ptr<InputStream>& st
return nullptr;
}
auto audioStream = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, videoStream, &status->audioCodec, 0);
if (audioStream < 0) {
if (audioStream == AVERROR_STREAM_NOT_FOUND) {
mprintf(("FFmpeg: No audio stream found in file!\n"));
} else if (audioStream == AVERROR_DECODER_NOT_FOUND) {
mprintf(("FFmpeg: Codec for audio stream could not be found!\n"));
} else {
mprintf(("FFmpeg: Unknown error while finding audio stream!\n"));
int audioStream = -1;
if (properties.with_audio) {
audioStream = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, videoStream, &status->audioCodec, 0);
if (audioStream < 0) {
if (audioStream == AVERROR_STREAM_NOT_FOUND) {
mprintf(("FFmpeg: No audio stream found in file!\n"));
} else if (audioStream == AVERROR_DECODER_NOT_FOUND) {
mprintf(("FFmpeg: Codec for audio stream could not be found!\n"));
} else {
mprintf(("FFmpeg: Unknown error while finding audio stream!\n"));
}
}
}
@@ -431,7 +444,8 @@ std::unique_ptr<InputStream> openSubtitleStream(const SCP_string& name)
}
}
bool FFMPEGDecoder::initialize(const SCP_string& fileName) {
bool FFMPEGDecoder::initialize(const SCP_string& fileName, const PlaybackProperties& properties)
{
SCP_string movieName = fileName;
// First make the file name lower case
std::transform(movieName.begin(), movieName.end(), movieName.begin(), ::tolower);
@@ -451,7 +465,7 @@ bool FFMPEGDecoder::initialize(const SCP_string& fileName) {
auto subt = openSubtitleStream(movieName);
// We now have a valid input stream, try to find the correct streams
auto status = initializeStatus(input, subt);
auto status = initializeStatus(input, subt, properties);
if (!status) {
return false;
}
@@ -463,6 +477,7 @@ bool FFMPEGDecoder::initialize(const SCP_string& fileName) {
std::swap(m_input, input);
std::swap(m_subtitleInput, subt);
std::swap(m_status, status);
m_properties = properties;
return true;
}
@@ -502,74 +517,93 @@ void FFMPEGDecoder::startDecoding() {
auto ctx = m_input->m_ctx->ctx();
AVPacket packet;
while (isDecoding()) {
auto read_err = av_read_frame(ctx, &packet);
AVPacketScope scope(&packet);
if (read_err < 0) {
if (read_err == AVERROR_EOF) {
// Finished reading -> break out of loop
break;
} else if (avio_feof(ctx->pb) != 0) {
// Also EOF!
break;
} else {
// Some kind of other error, try to continue reading
char errorStr[512];
av_strerror(read_err, errorStr, sizeof(errorStr));
mprintf(("FFMPEG: Failed to read frame! Error: %s\n", errorStr));
// Skip packet
continue;
}
do {
auto seek_err = av_seek_frame(ctx, -1, 0, AVSEEK_FLAG_BACKWARD);
if (seek_err < 0) {
char errorStr[512];
av_strerror(seek_err, errorStr, sizeof(errorStr));
mprintf(("FFMPEG: Failed to seek to start of movie file! Error: %s\n", errorStr));
break;
}
if (packet.stream_index == m_status->videoStreamIndex) {
videoDecoder->decodePacket(&packet);
VideoFramePtr ptr;
while((ptr = videoDecoder->getFrame()) != nullptr) {
pushFrameData(std::move(ptr));
}
} else if (audioDecoder && packet.stream_index == m_status->audioStreamIndex) {
audioDecoder->decodePacket(&packet);
AudioFramePtr ptr;
while ((ptr = audioDecoder->getFrame()) != nullptr) {
pushAudioData(std::move(ptr));
}
} else if (subtitleDecoder && packet.stream_index == m_status->subtitleStreamIndex) {
subtitleDecoder->decodePacket(&packet);
SubtitleFramePtr ptr;
while ((ptr = subtitleDecoder->getFrame()) != nullptr) {
pushSubtitleData(std::move(ptr));
}
}
}
if (isDecoding()) {
// If we are still alive then read the last frames from the decoders
videoDecoder->finishDecoding();
VideoFramePtr video_ptr;
while ((video_ptr = videoDecoder->getFrame()) != nullptr) {
pushFrameData(std::move(video_ptr));
}
videoDecoder->flushBuffers();
if (audioDecoder) {
audioDecoder->finishDecoding();
AudioFramePtr audio_frame;
while ((audio_frame = audioDecoder->getFrame()) != nullptr) {
pushAudioData(std::move(audio_frame));
audioDecoder->flushBuffers();
}
if (subtitleDecoder) {
subtitleDecoder->flushBuffers();
}
while (isDecoding()) {
auto read_err = av_read_frame(ctx, &packet);
AVPacketScope scope(&packet);
if (read_err < 0) {
if (read_err == AVERROR_EOF) {
// Finished reading -> break out of loop
break;
} else if (avio_feof(ctx->pb) != 0) {
// Also EOF!
break;
} else {
// Some kind of other error, try to continue reading
char errorStr[512];
av_strerror(read_err, errorStr, sizeof(errorStr));
mprintf(("FFMPEG: Failed to read frame! Error: %s\n", errorStr));
// Skip packet
continue;
}
}
if (packet.stream_index == m_status->videoStreamIndex) {
videoDecoder->decodePacket(&packet);
VideoFramePtr ptr;
while ((ptr = videoDecoder->getFrame()) != nullptr) {
pushFrameData(std::move(ptr));
}
} else if (audioDecoder && packet.stream_index == m_status->audioStreamIndex) {
audioDecoder->decodePacket(&packet);
AudioFramePtr ptr;
while ((ptr = audioDecoder->getFrame()) != nullptr) {
pushAudioData(std::move(ptr));
}
} else if (subtitleDecoder && packet.stream_index == m_status->subtitleStreamIndex) {
subtitleDecoder->decodePacket(&packet);
SubtitleFramePtr ptr;
while ((ptr = subtitleDecoder->getFrame()) != nullptr) {
pushSubtitleData(std::move(ptr));
}
}
}
}
if (isDecoding()) {
// If we are still alive then read the last frames from the decoders
videoDecoder->finishDecoding();
VideoFramePtr video_ptr;
while ((video_ptr = videoDecoder->getFrame()) != nullptr) {
pushFrameData(std::move(video_ptr));
}
if (audioDecoder) {
audioDecoder->finishDecoding();
AudioFramePtr audio_frame;
while ((audio_frame = audioDecoder->getFrame()) != nullptr) {
pushAudioData(std::move(audio_frame));
}
}
}
// If the video is being looped then we start again from the beginning
} while (m_properties.looping && isDecoding());
// Stop the decoder first to let the subtitle thread know that it shouldn't continue
stopDecoder();
if (subtitle_thread) {
subtitle_thread->join();
}
stopDecoder();
}
bool FFMPEGDecoder::hasAudio() const { return m_status->audioStreamIndex >= 0; }
+3 -1
View File
@@ -18,6 +18,8 @@ class FFMPEGDecoder: public Decoder {
std::unique_ptr<DecoderStatus> m_status;
PlaybackProperties m_properties;
bool hasExternalSubtitle() const;
void runSubtitleDecoder(SubtitleDecoder* decoder);
@@ -27,7 +29,7 @@ class FFMPEGDecoder: public Decoder {
~FFMPEGDecoder() override;
bool initialize(const SCP_string& fileName) override;
bool initialize(const SCP_string& fileName, const PlaybackProperties& properties) override;
MovieProperties getProperties() const override;
+3
View File
@@ -129,6 +129,9 @@ void SubtitleDecoder::pushSubtitleFrame(AVPacket* packet, AVSubtitle* subtitle)
pushFrame(std::move(frame));
}
void SubtitleDecoder::flushBuffers() {
avcodec_flush_buffers(m_status->subtitleCodecCtx);
}
}
}
+1
View File
@@ -16,6 +16,7 @@ class SubtitleDecoder: public FFMPEGStreamDecoder<SubtitleFrame> {
void finishDecoding() override;
void pushSubtitleFrame(AVPacket* subtitle, AVSubtitle* pSubtitle);
void flushBuffers() override;
};
}
+3
View File
@@ -158,5 +158,8 @@ void VideoDecoder::finishDecoding() {
}
#endif
}
void VideoDecoder::flushBuffers() {
avcodec_flush_buffers(m_status->videoCodecCtx);
}
}
}
+2
View File
@@ -21,6 +21,8 @@ class VideoDecoder: public FFMPEGStreamDecoder<VideoFrame> {
void decodePacket(AVPacket* packet) override;
void finishDecoding() override;
void flushBuffers() override;
};
}
}
+2
View File
@@ -74,6 +74,8 @@ class FFMPEGStreamDecoder {
virtual void finishDecoding() = 0;
virtual void flushBuffers() = 0;
virtual FramePtr<frame_type> getFrame();
};
+19 -8
View File
@@ -27,10 +27,10 @@ using namespace cutscene;
const int MAX_AUDIO_BUFFERS = 15;
std::unique_ptr<Decoder> findDecoder(const SCP_string& name) {
std::unique_ptr<Decoder> findDecoder(const SCP_string& name, const PlaybackProperties& properties) {
{
std::unique_ptr<Decoder> ffmpeg(new ::cutscene::ffmpeg::FFMPEGDecoder());
if (ffmpeg->initialize(name)) {
if (ffmpeg->initialize(name, properties)) {
return ffmpeg;
}
}
@@ -50,7 +50,8 @@ void videoPlaybackInit(PlayerState* state) {
Assert(state != NULL);
if (gr_screen.mode == GR_OPENGL) {
if (gr_screen.mode != GR_STUB) {
// The video presenter is independent of the underlying graphics API
state->videoPresenter.reset(new VideoPresenter(state->props));
}
@@ -122,6 +123,15 @@ void processVideoData(PlayerState* state) {
}
while (currentTime >= state->nextFrame->frameTime) {
if (state->currentFrame) {
if (state->nextFrame->frameTime < state->currentFrame->frameTime) {
// We reached the end of the movie and just looped to the beginning. Now we need to reset the playback
// time
state->playback_time = 0;
currentTime = playbackGetTime(state);
}
}
// Move the next frame to the current frame slot
state->currentFrame = std::move(state->nextFrame);
@@ -273,7 +283,8 @@ bool shouldBeginPlayback(Decoder* decoder) {
}
namespace cutscene {
Player::Player(std::unique_ptr<Decoder>&& decoder, bool enable_audio) : m_decoder(std::move(decoder))
Player::Player(std::unique_ptr<Decoder>&& decoder, const PlaybackProperties& properties)
: m_decoder(std::move(decoder))
{
Assertion(!m_state.videoInited,
"Internal State has been initialized before! Create a new player for replaying a movie.");
@@ -285,7 +296,7 @@ Player::Player(std::unique_ptr<Decoder>&& decoder, bool enable_audio) : m_decode
videoPlaybackInit(&m_state);
if (enable_audio) {
if (properties.with_audio) {
audioPlaybackInit(&m_state);
}
}
@@ -355,16 +366,16 @@ void Player::decoderThread() {
}
}
std::unique_ptr<Player> Player::newPlayer(const SCP_string& name, bool enable_audio)
std::unique_ptr<Player> Player::newPlayer(const SCP_string& name, const PlaybackProperties& properties)
{
mprintf(("Creating player for movie '%s'.\n", name.c_str()));
auto decoder = findDecoder(name);
auto decoder = findDecoder(name, properties);
if (decoder == nullptr) {
return nullptr;
}
return std::unique_ptr<Player>(new Player(std::move(decoder), enable_audio));
return std::unique_ptr<Player>(new Player(std::move(decoder), properties));
}
const PlayerState& Player::getInternalState() const {
return m_state;
+3 -4
View File
@@ -64,9 +64,8 @@ class Player {
private:
void decoderThread();
Player(std::unique_ptr<Decoder>&& decoder, const PlaybackProperties& properties);
public:
explicit Player(std::unique_ptr<Decoder>&& decoder, bool enable_audio = true);
~Player();
Player(const Player&) = delete;
@@ -130,9 +129,9 @@ class Player {
* @brief Creates a player
* The player is configured to play the movie with the specified name
* @param name The movie to play
* @param enable_audio Enable audio for this player instance
* @param properties A structure describing how the movie will be used
* @return @c nullptr if the specified movie could not be opened, a valid Player pointer otherwise
*/
static std::unique_ptr<Player> newPlayer(const SCP_string& name, bool enable_audio = true);
static std::unique_ptr<Player> newPlayer(const SCP_string& name, const PlaybackProperties& properties = PlaybackProperties());
};
}
-14
View File
@@ -103,20 +103,6 @@ typedef struct vec3d {
};
} vec3d;
/** Compares two vec3ds */
inline bool operator==(const vec3d &self, const vec3d &other)
{
return (self.xyz.x == other.xyz.x
&& self.xyz.y == other.xyz.y
&& self.xyz.z == other.xyz.z
);
}
inline bool operator!=(const vec3d &self, const vec3d &other)
{
return !(self == other);
}
typedef struct vec2d {
float x, y;
} vec2d;
+73
View File
@@ -495,6 +495,79 @@ vec3d vm_vec4_to_vec3(const vec4& vec);
*/
vec4 vm_vec3_to_ve4(const vec3d& vec, float w = 1.0f);
/** Compares two vec3ds */
inline bool operator==(const vec3d& left, const vec3d& right) { return vm_vec_same(&left, &right) != 0; }
inline bool operator!=(const vec3d& left, const vec3d& right) { return !(left == right); }
inline vec3d operator+(const vec3d& left, const vec3d& right)
{
vec3d res;
vm_vec_add(&res, &left, &right);
return res;
}
inline vec3d& operator+=(vec3d& left, const vec3d& right)
{
vm_vec_add2(&left, &right);
return left;
}
inline vec3d operator-(const vec3d& left, const vec3d& right)
{
vec3d res;
vm_vec_sub(&res, &left, &right);
return res;
}
inline vec3d& operator-=(vec3d& left, const vec3d& right)
{
vm_vec_sub2(&left, &right);
return left;
}
inline vec3d operator*(vec3d& left, float right)
{
vec3d out;
vm_vec_copy_scale(&out, &left, right);
return out;
}
inline vec3d& operator*=(vec3d& left, float right)
{
vm_vec_scale(&left, right);
return left;
}
inline vec3d operator/(vec3d& left, float right)
{
vec3d out;
vm_vec_copy_scale(&out, &left, 1.0f / right);
return out;
}
inline vec3d& operator/=(vec3d& left, float right)
{
vm_vec_scale(&left, 1.0f / right);
return left;
}
/**
* @brief Rotates a vector into the orientation specified by the matrix
* @param left The matrix
* @param right The vector
* @return The rotated vector
*
* @note This actually follows the definition of the * operator in linear algebra. The standard vm_vec_rotate actually
* implements a multiplication with the transpose of the matrix.
*/
inline vec3d operator*(const matrix& left, const vec3d& right) {
vec3d out;
vm_vec_unrotate(&out, &right, &left);
return out;
}
inline matrix operator*(const matrix& left, const matrix& right) {
matrix out;
vm_matrix_x_matrix(&out, &left, &right);
return out;
}
#endif
+8 -3
View File
@@ -1541,18 +1541,23 @@ ADE_FUNC(resetClip, l_Graphics, NULL, "Resets the clipping region that might hav
return ADE_RETURN_TRUE;
}
ADE_FUNC(openMovie, l_Graphics, "string name",
ADE_FUNC(openMovie, l_Graphics, "string name, boolean looping = false",
"Opens the movie with the specified name. If the name has an extension it will be removed. This function will "
"try all movie formats supported by the engine and use the first that is found.",
"movie_player", "The cutscene player handle or invalid handle if cutscene could not be opened.")
{
const char* name = nullptr;
if (!ade_get_args(L, "s", &name)) {
bool looping = false;
if (!ade_get_args(L, "s|b", &name, &looping)) {
return ade_set_error(L, "o", l_MoviePlayer.Set(nullptr));
}
// Audio is disabled for scripted movies at the moment
auto player = cutscene::Player::newPlayer(name, false);
cutscene::PlaybackProperties props;
props.with_audio = false;
props.looping = looping;
auto player = cutscene::Player::newPlayer(name, props);
if (!player) {
return ade_set_args(L, "o", l_MoviePlayer.Set(nullptr));
+4 -1
View File
@@ -308,7 +308,10 @@ bool fred_init(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
cfile_chdir(Fred_base_dir);
// this should enable mods - Kazan
parse_cmdline(__argc, __argv);
if (!parse_cmdline(__argc, __argv)) {
// Command line contained an option that terminates the program immediately
exit(1);
}
#ifndef NDEBUG
#if FS_VERSION_REVIS == 0
+5
View File
@@ -38,6 +38,8 @@ add_file_folder("Source/Mission"
add_file_folder("Source/Mission/Dialogs"
src/mission/dialogs/AbstractDialogModel.cpp
src/mission/dialogs/AbstractDialogModel.h
src/mission/dialogs/AsteroidEditorDialogModel.cpp
src/mission/dialogs/AsteroidEditorDialogModel.h
src/mission/dialogs/CustomWingNamesDialogModel.cpp
src/mission/dialogs/CustomWingNamesDialogModel.h
src/mission/dialogs/FictionViewerDialogModel.cpp
@@ -68,6 +70,8 @@ add_file_folder("Source/UI"
add_file_folder("Source/UI/Dialogs"
src/ui/dialogs/AboutDialog.cpp
src/ui/dialogs/AboutDialog.h
src/ui/dialogs/AsteroidEditorDialog.cpp
src/ui/dialogs/AsteroidEditorDialog.h
src/ui/dialogs/BackgroundEditorDialog.h
src/ui/dialogs/BackgroundEditorDialog.cpp
src/ui/dialogs/BriefingEditorDialog.cpp
@@ -116,6 +120,7 @@ add_file_folder("Source/UI/Widgets"
add_file_folder("UI"
ui/AboutDialog.ui
ui/AsteroidEditorDialog.ui
ui/BackgroundEditor.ui
ui/BriefingEditorDialog.ui
ui/CustomWingNamesDialog.ui
@@ -0,0 +1,458 @@
#include "mission/dialogs/AsteroidEditorDialogModel.h"
namespace fso {
namespace fred {
namespace dialogs {
AsteroidEditorDialogModel::AsteroidEditorDialogModel(QObject* parent, EditorViewport* viewport) :
AbstractDialogModel(parent, viewport),
_enable_asteroids(false),
_enable_inner_bounds(false),
_num_asteroids(0),
_avg_speed(0),
_min_x(""),
_min_y(""),
_min_z(""),
_max_x(""),
_max_y(""),
_max_z(""),
_inner_min_x(""),
_inner_min_y(""),
_inner_min_z(""),
_inner_max_x(""),
_inner_max_y(""),
_inner_max_z(""),
_field_type(FT_ACTIVE),
_debris_genre(DG_ASTEROID),
_bypass_errors(false),
_modified(false),
_cur_field(0),
_last_field(-1)
{
for (auto i = 0ul; i < ship_debris_idx_lookup.size(); ++i) {
debris_inverse_idx_lookup.emplace(ship_debris_idx_lookup[i], i);
}
// note that normal asteroids use the same index field! Need to add dummy entries for them as well
for (auto i = 0; i < 3; ++i) {
debris_inverse_idx_lookup.emplace(i, 0);
}
initializeData();
}
bool AsteroidEditorDialogModel::apply()
{
update_init();
if (!AsteroidEditorDialogModel::validate_data()) {
return false;
}
Asteroid_field = _a_field;
return true;
}
void AsteroidEditorDialogModel::reject()
{
//do nothing - only here because parent class reject() function is virtual
}
void AsteroidEditorDialogModel::initializeData()
{
for (auto i = 0; i < MAX_ACTIVE_DEBRIS_TYPES; ++i) {
_field_debris_type[i] = -1;
}
_a_field = Asteroid_field;
}
void AsteroidEditorDialogModel::setEnabled(bool enabled)
{
_enable_asteroids = enabled;
}
bool AsteroidEditorDialogModel::getEnabled()
{
return _enable_asteroids;
}
void AsteroidEditorDialogModel::setInnerBoxEnabled(bool enabled)
{
_enable_inner_bounds = enabled;
}
bool AsteroidEditorDialogModel::getInnerBoxEnabled()
{
return _enable_inner_bounds;
}
void AsteroidEditorDialogModel::setAsteroidEnabled(_roid_types type, bool enabled)
{
Assertion(type >=0 && type < MAX_ACTIVE_DEBRIS_TYPES, "Invalid Asteroid checkbox type: %i\n", type);
modify(_field_debris_type[type], enabled == true ? 1 : -1);
}
bool AsteroidEditorDialogModel::getAsteroidEnabled(_roid_types type)
{
Assertion(type >=0 && type < MAX_ACTIVE_DEBRIS_TYPES, "Invalid Asteroid checkbox type: %i\n", type);
return (_field_debris_type[type] == 1);
}
void AsteroidEditorDialogModel::setNumAsteroids(int num_asteroids)
{
modify(_num_asteroids, num_asteroids);
}
int AsteroidEditorDialogModel::getNumAsteroids()
{
return _num_asteroids;
}
QString & AsteroidEditorDialogModel::getBoxText(_box_line_edits type)
{
switch (type) {
case _O_MIN_X: return _min_x;
case _O_MIN_Y: return _min_y;
case _O_MIN_Z: return _min_z;
case _O_MAX_X: return _max_x;
case _O_MAX_Y: return _max_y;
case _O_MAX_Z: return _max_z;
case _I_MIN_X: return _inner_min_x;
case _I_MIN_Y: return _inner_min_y;
case _I_MIN_Z: return _inner_min_z;
case _I_MAX_X: return _inner_max_x;
case _I_MAX_Y: return _inner_max_y;
case _I_MAX_Z: return _inner_max_z;
default:
Error(LOCATION, "Get a coder! Unknown enum value found! %i", type);
return _min_x;
}
}
void AsteroidEditorDialogModel::setBoxText(const QString &text, _box_line_edits type)
{
switch (type) {
case _O_MIN_X: modify(_min_x, text); break;
case _O_MIN_Y: modify(_min_y, text); break;
case _O_MIN_Z: modify(_min_z, text); break;
case _O_MAX_X: modify(_max_x, text); break;
case _O_MAX_Y: modify(_max_y, text); break;
case _O_MAX_Z: modify(_max_z, text); break;
case _I_MIN_X: modify(_inner_min_x, text); break;
case _I_MIN_Y: modify(_inner_min_y, text); break;
case _I_MIN_Z: modify(_inner_min_z, text); break;
case _I_MAX_X: modify(_inner_max_x, text); break;
case _I_MAX_Y: modify(_inner_max_y, text); break;
case _I_MAX_Z: modify(_inner_max_z, text); break;
default:
Error(LOCATION, "Get a coder! Unknown enum value found! %i", type);
break;
}
}
void AsteroidEditorDialogModel::setDebrisGenre(debris_genre_t genre)
{
modify(_debris_genre, genre);
}
debris_genre_t AsteroidEditorDialogModel::getDebrisGenre()
{
return _debris_genre;
}
void AsteroidEditorDialogModel::setFieldType(field_type_t type)
{
modify(_field_type, type);
}
field_type_t AsteroidEditorDialogModel::getFieldType()
{
return _field_type;
}
void AsteroidEditorDialogModel::setFieldDebrisType(int idx, int debris_type)
{
Assertion(idx >= 0 && idx < MAX_ACTIVE_DEBRIS_TYPES, "Invalid debris index provided: %i\n", idx);
modify(_field_debris_type[idx], ship_debris_idx_lookup.at(debris_type));
}
int AsteroidEditorDialogModel::getFieldDebrisType(int idx)
{
Assertion(idx >= 0 && idx < MAX_ACTIVE_DEBRIS_TYPES, "Invalid debris index provided: %i\n", idx);
return debris_inverse_idx_lookup.at(_field_debris_type[idx]);
}
void AsteroidEditorDialogModel::setAvgSpeed(int speed)
{
modify(_avg_speed, speed);
}
QString AsteroidEditorDialogModel::getAvgSpeed()
{
return QString::number(_avg_speed);
}
bool AsteroidEditorDialogModel::validate_data()
{
if (!_enable_asteroids) {
return true;
}
else {
// be helpful to the FREDer; try to advise precisely what the problem is
// more general checks 1st, followed by more specific ones
_bypass_errors = false;
// check outer x/y/z max is greater than min
if (_a_field.max_bound.xyz.x < _a_field.min_bound.xyz.x) {
showErrorDialogNoCancel( "Outer box 'X' min is greater than max\n");
return false;
}
// check y
if (_a_field.max_bound.xyz.y < _a_field.min_bound.xyz.y) {
showErrorDialogNoCancel( "Outer box 'Y' min is greater than max\n");
return false;
}
// check z
if (_a_field.max_bound.xyz.z < _a_field.min_bound.xyz.z) {
showErrorDialogNoCancel( "Outer box 'Z' min is greater than max\n");
return false;
}
if (_a_field.has_inner_bound) {
// check inner x/y/z max is greater than min
if (_a_field.inner_max_bound.xyz.x < _a_field.inner_min_bound.xyz.x) {
showErrorDialogNoCancel( "Inner box 'X' min is greater than inner max\n");
return false;
}
if (_a_field.inner_max_bound.xyz.y < _a_field.inner_min_bound.xyz.y) {
showErrorDialogNoCancel( "Inner box 'Y' min is greater than inner max\n");
return false;
}
if (_a_field.inner_max_bound.xyz.z < _a_field.inner_min_bound.xyz.z) {
showErrorDialogNoCancel( "Inner box 'Z' min is greater than inner max\n");
return false;
}
// check outer x/y/z max is greater than inner x/y/z max
if (_a_field.max_bound.xyz.x < _a_field.inner_max_bound.xyz.x) {
showErrorDialogNoCancel( "Outer box 'X' max is less than inner max\n");
return false;
}
if (_a_field.max_bound.xyz.y < _a_field.inner_max_bound.xyz.y) {
showErrorDialogNoCancel( "Outer box 'Y' max is less than inner max\n");
return false;
}
if (_a_field.max_bound.xyz.z < _a_field.inner_max_bound.xyz.z) {
showErrorDialogNoCancel( "Outer box 'Z' max is less than inner max\n");
return false;
}
// check outer x/y/z min is less than inner x/y/z min
if (_a_field.min_bound.xyz.x > _a_field.inner_min_bound.xyz.x) {
showErrorDialogNoCancel( "Inner box 'X' min is less than outer min\n");
return false;
}
if (_a_field.min_bound.xyz.y > _a_field.inner_min_bound.xyz.y) {
showErrorDialogNoCancel( "Inner box 'Y' min is less than outer min\n");
return false;
}
if (_a_field.min_bound.xyz.z > _a_field.inner_min_bound.xyz.z) {
showErrorDialogNoCancel( "Inner box 'Z' min is less than outer min\n");
return false;
}
// split checks to give FREDers more specific feedback
// check x thickness
if (_a_field.inner_min_bound.xyz.x - _MIN_BOX_THICKNESS < _a_field.min_bound.xyz.x) {
showErrorDialogNoCancel(
"X axis minimum values must be at least " + \
std::to_string(_MIN_BOX_THICKNESS) + " apart");
return false;
}
if (_a_field.inner_max_bound.xyz.x + _MIN_BOX_THICKNESS > _a_field.max_bound.xyz.x) {
showErrorDialogNoCancel(
"X axis maximum values must be at least " + \
std::to_string(_MIN_BOX_THICKNESS) + " apart");
return false;
}
// check y thickness
if (_a_field.inner_min_bound.xyz.y - _MIN_BOX_THICKNESS < _a_field.min_bound.xyz.y) {
showErrorDialogNoCancel(
"Y axis minimum values must be at least " + \
std::to_string(_MIN_BOX_THICKNESS) + " apart");
return false;
}
if (_a_field.inner_max_bound.xyz.y + _MIN_BOX_THICKNESS > _a_field.max_bound.xyz.y) {
showErrorDialogNoCancel(
"Y axis maximum values must be at least " + \
std::to_string(_MIN_BOX_THICKNESS) + " apart");
return false;
}
// check z thickness
if (_a_field.inner_min_bound.xyz.z - _MIN_BOX_THICKNESS < _a_field.min_bound.xyz.z) {
showErrorDialogNoCancel(
"Z axis minimum values must be at least " + \
std::to_string(_MIN_BOX_THICKNESS) + " apart");
return false;
}
if (_a_field.inner_max_bound.xyz.z + _MIN_BOX_THICKNESS > _a_field.max_bound.xyz.z) {
showErrorDialogNoCancel(
"Z axis maximum values must be at least " + \
std::to_string(_MIN_BOX_THICKNESS) + " apart");
return false;
}
}
// for a ship debris (i.e. passive) field, need at least one debris type is selected
if (_a_field.field_type == FT_PASSIVE) {
if (_a_field.debris_genre == DG_SHIP) {
if ( (_a_field.field_debris_type[0] == -1) && \
(_a_field.field_debris_type[1] == -1) && \
(_a_field.field_debris_type[2] == -1) ) {
showErrorDialogNoCancel("You must choose one or more types of ship debris\n");
return false;
}
}
}
// check at least one asteroid subtype is selected
if (_a_field.debris_genre == DG_ASTEROID) {
if ( (_a_field.field_debris_type[_AST_BROWN] == -1) && \
(_a_field.field_debris_type[_AST_BLUE] == -1) && \
(_a_field.field_debris_type[_AST_ORANGE] == -1) ) {
showErrorDialogNoCancel("You must choose one or more asteroid subtypes\n");
return false;
}
}
}
return true;
}
void AsteroidEditorDialogModel::update_init()
{
int num_asteroids;
if (_last_field >= 0) {
// store into temp asteroid field
num_asteroids = _a_field.num_initial_asteroids;
_a_field.num_initial_asteroids = _enable_asteroids ? _num_asteroids : 0;
CLAMP(_a_field.num_initial_asteroids, 0, MAX_ASTEROIDS);
if (num_asteroids != _a_field.num_initial_asteroids) {
set_modified();
}
vec3d vel_vec {{{ 1.0f, 0.0f, 0.0f }}};
vm_vec_scale(&vel_vec, static_cast<float>(_avg_speed));
modify(_a_field.vel, vel_vec);
// save the box coords
modify(_a_field.min_bound.xyz.x, _min_x.toFloat());
modify(_a_field.min_bound.xyz.y, _min_y.toFloat());
modify(_a_field.min_bound.xyz.z, _min_z.toFloat());
modify(_a_field.max_bound.xyz.x, _max_x.toFloat());
modify(_a_field.max_bound.xyz.y, _max_y.toFloat());
modify(_a_field.max_bound.xyz.z, _max_z.toFloat());
modify(_a_field.inner_min_bound.xyz.x, _inner_min_x.toFloat());
modify(_a_field.inner_min_bound.xyz.y, _inner_min_y.toFloat());
modify(_a_field.inner_min_bound.xyz.z, _inner_min_z.toFloat());
modify(_a_field.inner_max_bound.xyz.x, _inner_max_x.toFloat());
modify(_a_field.inner_max_bound.xyz.y, _inner_max_y.toFloat());
modify(_a_field.inner_max_bound.xyz.z, _inner_max_z.toFloat());
// type of field
modify(_a_field.field_type, _field_type);
modify(_a_field.debris_genre, _debris_genre);
// ship debris
if ( (_field_type == FT_PASSIVE) && (_debris_genre == DG_SHIP) ) {
for (auto idx=0; idx<MAX_ACTIVE_DEBRIS_TYPES; ++idx) {
modify(_a_field.field_debris_type[idx], _field_debris_type[idx]);
}
}
// asteroids
if ( _debris_genre == DG_ASTEROID ) {
modify(_a_field.field_debris_type[_AST_BROWN], getAsteroidEnabled(_AST_BROWN) == true ? 1 : -1);
modify(_a_field.field_debris_type[_AST_BLUE], getAsteroidEnabled(_AST_BLUE) == true ? 1 : -1);
modify(_a_field.field_debris_type[_AST_ORANGE], getAsteroidEnabled(_AST_ORANGE) == true ? 1 : -1);
}
modify(_a_field.has_inner_bound, static_cast<int>(_enable_inner_bounds));
}
// get from temp asteroid field into class
_enable_asteroids = _a_field.num_initial_asteroids ? true : false;
_enable_inner_bounds = _a_field.has_inner_bound ? true : false;
_num_asteroids = _a_field.num_initial_asteroids;
if (!_enable_asteroids) {
_num_asteroids = 10;
}
// set field type
_field_type = _a_field.field_type;
_debris_genre = _a_field.debris_genre;
_avg_speed = static_cast<int>(vm_vec_mag(&_a_field.vel));
_min_x = QString::number(_a_field.min_bound.xyz.x, 'f', 1);
_min_y = QString::number(_a_field.min_bound.xyz.y, 'f', 1);
_min_z = QString::number(_a_field.min_bound.xyz.z, 'f', 1);
_max_x = QString::number(_a_field.max_bound.xyz.x, 'f', 1);
_max_y = QString::number(_a_field.max_bound.xyz.y, 'f', 1);
_max_z = QString::number(_a_field.max_bound.xyz.z, 'f', 1);
_inner_min_x = QString::number(_a_field.inner_min_bound.xyz.x, 'f', 1);
_inner_min_y = QString::number(_a_field.inner_min_bound.xyz.y, 'f', 1);
_inner_min_z = QString::number(_a_field.inner_min_bound.xyz.z, 'f', 1);
_inner_max_x = QString::number(_a_field.inner_max_bound.xyz.x, 'f', 1);
_inner_max_y = QString::number(_a_field.inner_max_bound.xyz.y, 'f', 1);
_inner_max_z = QString::number(_a_field.inner_max_bound.xyz.z, 'f', 1);
// ship debris or asteroids
for (auto i = 0; i < MAX_ACTIVE_DEBRIS_TYPES; ++i) {
_field_debris_type[i] = _a_field.field_debris_type[i];
}
_last_field = _cur_field;
}
void AsteroidEditorDialogModel::set_modified()
{
_modified = true;
}
void AsteroidEditorDialogModel::unset_modified()
{
_modified = false;
}
bool AsteroidEditorDialogModel::get_modified()
{
return _modified;
}
void AsteroidEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message)
{
if (_bypass_errors) {
return;
}
_bypass_errors = true;
_viewport->dialogProvider->showButtonDialog(DialogType::Error,
"Error",
message,
{ DialogButton::Ok });
}
} // namespace dialogs
} // namespace fred
} // namespace fso
@@ -0,0 +1,120 @@
#pragma once
#include "mission/dialogs/AbstractDialogModel.h"
#include "asteroid/asteroid.h"
namespace fso {
namespace fred {
namespace dialogs {
class AsteroidEditorDialogModel: public AbstractDialogModel {
Q_OBJECT
public:
AsteroidEditorDialogModel(QObject* parent, EditorViewport* viewport);
enum _box_line_edits {
_I_MIN_X =0,
_I_MIN_Y,
_I_MIN_Z,
_I_MAX_X,
_I_MAX_Y,
_I_MAX_Z,
_O_MIN_X,
_O_MIN_Y,
_O_MIN_Z,
_O_MAX_X,
_O_MAX_Y,
_O_MAX_Z,
};
enum _roid_types {
_AST_BROWN =0,
_AST_BLUE =1,
_AST_ORANGE =2,
};
bool apply() override;
void reject() override;
void setEnabled(bool enabled);
bool getEnabled();
void setInnerBoxEnabled(bool enabled);
bool getInnerBoxEnabled();
void setAsteroidEnabled(_roid_types type, bool enabled);
bool getAsteroidEnabled(_roid_types type);
void setNumAsteroids(int num_asteroids);
int getNumAsteroids();
void setDebrisGenre(debris_genre_t genre);
debris_genre_t getDebrisGenre();
void setFieldType(field_type_t type);
field_type_t getFieldType();
void setFieldDebrisType(int idx, int num_asteroids);
int getFieldDebrisType(int idx);
void setAvgSpeed(int speed);
QString getAvgSpeed();
void setBoxText(const QString &text, _box_line_edits type);
QString & getBoxText(_box_line_edits type);
void update_init();
bool validate_data();
void set_modified();
void unset_modified();
bool get_modified();
private:
template<typename T>
void modify(T &a, const T &b);
void showErrorDialogNoCancel(const SCP_string& message);
void initializeData();
bool _enable_asteroids;
bool _enable_inner_bounds;
int _num_asteroids;
int _avg_speed;
QString _min_x;
QString _min_y;
QString _min_z;
QString _max_x;
QString _max_y;
QString _max_z;
QString _inner_min_x;
QString _inner_min_y;
QString _inner_min_z;
QString _inner_max_x;
QString _inner_max_y;
QString _inner_max_z;
int _field_debris_type[MAX_ACTIVE_DEBRIS_TYPES]; // species and size of ship debris
field_type_t _field_type; // active or passive
debris_genre_t _debris_genre; // ship or asteroid
asteroid_field _a_field; // :v: had unfinished plans for multiple fields?
bool _bypass_errors;
bool _modified;
int _cur_field;
int _last_field;
const int _MIN_BOX_THICKNESS = 400;
// for debris combo box indexes
// -1 == none, 3 == terran debris (small), etc to 11 == shivan debris (large)
const std::array<int, 10> ship_debris_idx_lookup{ {-1, 3, 4, 5, 6, 7, 8, 9, 10, 11} };
// and the inverse as a map + roids - populate in ctor
std::unordered_map<int, int> debris_inverse_idx_lookup;
};
template<typename T>
inline void AsteroidEditorDialogModel::modify(T &a, const T &b) {
if (a != b) {
a = b;
set_modified();
modelChanged();
}
}
} // namespace dialogs
} // namespace fred
} // namespace fso
+4 -1
View File
@@ -66,7 +66,10 @@ initialize(const std::string& cfilepath, int argc, char* argv[], Editor* editor,
listener(SubSystem::CommandLine);
// this should enable mods - Kazan
parse_cmdline(argc, argv);
if (!parse_cmdline(argc, argv)) {
// Command line contained an option that terminates the program immediately
return false;
}
#ifndef NDEBUG
#if FS_VERSION_REVIS == 0
+5
View File
@@ -13,6 +13,7 @@
#include <FredApplication.h>
#include <ui/dialogs/EventEditorDialog.h>
#include <ui/dialogs/AsteroidEditorDialog.h>
#include <ui/dialogs/BriefingEditorDialog.h>
#include <ui/dialogs/WaypointEditorDialog.h>
#include <ui/dialogs/MissionGoalsDialog.h>
@@ -672,6 +673,10 @@ void FredView::onUpdateShipClassBox() {
void FredView::onShipClassSelected(int ship_class) {
_viewport->cur_model_index = ship_class;
}
void FredView::on_actionAsteroid_Field_triggered(bool) {
auto asteroidFieldEditor = new dialogs::AsteroidEditorDialog(this, _viewport);
asteroidFieldEditor->show();
}
void FredView::on_actionBriefing_triggered(bool) {
auto eventEditor = new dialogs::BriefingEditorDialog(this);
eventEditor->show();
+1
View File
@@ -80,6 +80,7 @@ class FredView: public QMainWindow, public IDialogProvider {
void on_actionCurrent_Ship_triggered(bool enabled);
void on_actionEvents_triggered(bool);
void on_actionAsteroid_Field_triggered(bool);
void on_actionBriefing_triggered(bool);
void on_actionMission_Specs_triggered(bool);
void on_actionWaypoint_Paths_triggered(bool);
@@ -0,0 +1,369 @@
#include "ui/dialogs/AsteroidEditorDialog.h"
#include "ui/util/SignalBlockers.h"
#include <algorithm>
#include "ui_AsteroidEditorDialog.h"
namespace fso {
namespace fred {
namespace dialogs {
static bool sort_qcombobox_by_name(const QComboBox *left, const QComboBox *right)
{
Assertion(left != nullptr && right != nullptr, "Don't pass nullptr's to sort!\n");
return left->objectName() < right->objectName();
}
AsteroidEditorDialog::AsteroidEditorDialog(FredView *parent, EditorViewport* viewport) :
QDialog(parent),
_viewport(viewport),
_editor(viewport->editor),
ui(new Ui::AsteroidEditorDialog()),
_model(new AsteroidEditorDialogModel(this, viewport))
{
ui->setupUi(this);
_model->update_init();
// checkboxes
connect(ui->enabled, &QCheckBox::toggled, this, &AsteroidEditorDialog::toggleEnabled);
connect(ui->innerBoxEnabled, &QCheckBox::toggled, this, &AsteroidEditorDialog::toggleInnerBoxEnabled);
connect(ui->checkBoxBrown, &QCheckBox::toggled, this,
[this](bool enabled) { \
AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_AST_BROWN, enabled); });
connect(ui->checkBoxBlue, &QCheckBox::toggled, this,
[this](bool enabled) { \
AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_AST_BLUE, enabled); });
connect(ui->checkBoxOrange, &QCheckBox::toggled, this,
[this](bool enabled) { \
AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_AST_ORANGE, enabled); });
// (come in) spinners
ui->spinBoxNumber->setRange(1, MAX_ASTEROIDS);
ui->spinBoxNumber->setValue(_model->getNumAsteroids());
// only connect once we're done setting values or unwanted signal's will be sent
connect(ui->spinBoxNumber, QOverload<int>::of(&QSpinBox::valueChanged), this, \
&AsteroidEditorDialog::asteroidNumberChanged);
// setup values in ship debris combo boxes
// MFC let you set comboxbox item indexes, Qt doesn't so we'll need a lookup
static_assert(MAX_ACTIVE_DEBRIS_TYPES == 3,
"qtFRED only provides three combo boxes for debris type input");
debrisComboBoxes = ui->fieldProperties->findChildren<QComboBox *>(QString(), Qt::FindDirectChildrenOnly);
std::sort(debrisComboBoxes.begin(), debrisComboBoxes.end(), sort_qcombobox_by_name);
QString debris_size[NUM_DEBRIS_SIZES] = { "Small", "Medium", "Large" };
QStringList debris_names("None");
for (auto i = 0ul; i < Species_info.size(); i++) // each species
{
for (auto j = 0; j < NUM_DEBRIS_SIZES; j++) // each size
{
debris_names += QString(Species_info.at(i).species_name) + " " + debris_size[j];
}
}
for (auto i = 0; i < MAX_ACTIVE_DEBRIS_TYPES; ++i) {
debrisComboBoxes.at(i)->addItems(debris_names);
// update debris combobox data on index changes
connect(debrisComboBoxes.at(i), QOverload<int>::of(&QComboBox::currentIndexChanged), this, \
[this, i](int debris_type) { AsteroidEditorDialog::updateComboBox(i,debris_type); });
}
// radio buttons
connect(ui->radioButtonActiveField, &QRadioButton::toggled, this, &AsteroidEditorDialog::setFieldActive);
connect(ui->radioButtonPassiveField, &QRadioButton::toggled, this, &AsteroidEditorDialog::setFieldPassive);
connect(ui->radioButtonAsteroid, &QRadioButton::toggled, this, &AsteroidEditorDialog::setGenreAsteroid);
connect(ui->radioButtonShip, &QRadioButton::toggled, this, &AsteroidEditorDialog::setGenreShip);
// lineEdit signals/slots
connect(ui->lineEdit_obox_minX, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextOMinX);
connect(ui->lineEdit_obox_minY, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextOMinY);
connect(ui->lineEdit_obox_minZ, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextOMinZ);
connect(ui->lineEdit_obox_maxX, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextOMaxX);
connect(ui->lineEdit_obox_maxY, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextOMaxY);
connect(ui->lineEdit_obox_maxZ, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextOMaxZ);
connect(ui->lineEdit_ibox_minX, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextIMinX);
connect(ui->lineEdit_ibox_minY, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextIMinY);
connect(ui->lineEdit_ibox_minZ, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextIMinZ);
connect(ui->lineEdit_ibox_maxX, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextIMaxX);
connect(ui->lineEdit_ibox_maxY, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextIMaxY);
connect(ui->lineEdit_ibox_maxZ, &QLineEdit::textEdited, this, \
&AsteroidEditorDialog::changedBoxTextIMaxZ);
// setup validators for text input
_box_validator.setNotation(QDoubleValidator::StandardNotation);
_box_validator.setDecimals(1);
ui->lineEdit_obox_minX->setValidator(&_box_validator);
ui->lineEdit_obox_minY->setValidator(&_box_validator);
ui->lineEdit_obox_minZ->setValidator(&_box_validator);
ui->lineEdit_obox_maxX->setValidator(&_box_validator);
ui->lineEdit_obox_maxY->setValidator(&_box_validator);
ui->lineEdit_obox_maxZ->setValidator(&_box_validator);
ui->lineEdit_ibox_minX->setValidator(&_box_validator);
ui->lineEdit_ibox_minY->setValidator(&_box_validator);
ui->lineEdit_ibox_minZ->setValidator(&_box_validator);
ui->lineEdit_ibox_maxX->setValidator(&_box_validator);
ui->lineEdit_ibox_maxY->setValidator(&_box_validator);
ui->lineEdit_ibox_maxZ->setValidator(&_box_validator);
ui->lineEditAvgSpeed->setValidator(&_speed_validator);
updateUI();
}
AsteroidEditorDialog::~AsteroidEditorDialog()
{
}
QString & AsteroidEditorDialog::getBoxText(AsteroidEditorDialogModel::_box_line_edits type)
{
return _model->getBoxText(type);
}
void AsteroidEditorDialog::changedBoxTextIMinX(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_X);
}
void AsteroidEditorDialog::changedBoxTextIMinY(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Y);
}
void AsteroidEditorDialog::changedBoxTextIMinZ(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Z);
}
void AsteroidEditorDialog::changedBoxTextIMaxX(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_X);
}
void AsteroidEditorDialog::changedBoxTextIMaxY(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Y);
}
void AsteroidEditorDialog::changedBoxTextIMaxZ(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Z);
}
void AsteroidEditorDialog::changedBoxTextOMinX(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_X);
}
void AsteroidEditorDialog::changedBoxTextOMinY(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Y);
}
void AsteroidEditorDialog::changedBoxTextOMinZ(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Z);
}
void AsteroidEditorDialog::changedBoxTextOMaxX(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_X);
}
void AsteroidEditorDialog::changedBoxTextOMaxY(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Y);
}
void AsteroidEditorDialog::changedBoxTextOMaxZ(const QString &text)
{
_model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Z);
}
void AsteroidEditorDialog::setFieldActive()
{
_model->setFieldType(FT_ACTIVE);
setGenreAsteroid(); // only allow asteroids in active fields
updateUI();
}
void AsteroidEditorDialog::setFieldPassive()
{
_model->setFieldType(FT_PASSIVE);
updateUI();
}
void AsteroidEditorDialog::setGenreAsteroid()
{
_model->setDebrisGenre(DG_ASTEROID);
updateUI();
}
void AsteroidEditorDialog::setGenreShip()
{
_model->setDebrisGenre(DG_SHIP);
updateUI();
}
void AsteroidEditorDialog::toggleEnabled(bool enabled)
{
_model->setEnabled(enabled);
updateUI();
}
void AsteroidEditorDialog::toggleInnerBoxEnabled(bool enabled)
{
_model->setInnerBoxEnabled(enabled);
updateUI();
}
void AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_roid_types colour, bool enabled)
{
_model->setAsteroidEnabled(colour, enabled);
updateUI();
}
void AsteroidEditorDialog::asteroidNumberChanged(int num_asteroids)
{
_model->setNumAsteroids(num_asteroids);
}
void AsteroidEditorDialog::updateComboBox(int idx, int debris_type)
{
_model->setFieldDebrisType(idx, debris_type);
}
void AsteroidEditorDialog::updateUI()
{
util::SignalBlockers blockers(this);
// various useful states
bool asteroids_enabled = _model->getEnabled();
bool inner_box_enabled = _model->getInnerBoxEnabled();
bool field_is_active = (_model->getFieldType() == FT_ACTIVE);
bool debris_is_asteroid = (_model->getDebrisGenre() == DG_ASTEROID);
// checkboxes
ui->enabled->setChecked(asteroids_enabled);
ui->innerBoxEnabled->setChecked(inner_box_enabled);
ui->checkBoxBrown->setChecked(_model->getAsteroidEnabled(AsteroidEditorDialogModel::_AST_BROWN));
ui->checkBoxBlue->setChecked(_model->getAsteroidEnabled(AsteroidEditorDialogModel::_AST_BLUE));
ui->checkBoxOrange->setChecked(_model->getAsteroidEnabled(AsteroidEditorDialogModel::_AST_ORANGE));
// radio buttons (2x groups)
ui->radioButtonActiveField->setChecked(field_is_active);
ui->radioButtonPassiveField->setChecked(!field_is_active);
ui->radioButtonAsteroid->setChecked(debris_is_asteroid);
ui->radioButtonShip->setChecked(!debris_is_asteroid);
if (field_is_active) {
ui->radioButtonShip->setToolTip(QString("Ship Debris is only allowed in passive fields"));
}
else {
ui->radioButtonShip->setToolTip(QString(""));
}
// enable/disable sections of interface
ui->fieldProperties->setEnabled(asteroids_enabled);
ui->outerBox->setEnabled(asteroids_enabled);
ui->innerBox->setEnabled(asteroids_enabled);
ui->innerBoxEnabled->setEnabled(asteroids_enabled && field_is_active);
ui->lineEdit_ibox_maxX->setEnabled(inner_box_enabled && field_is_active);
ui->lineEdit_ibox_maxY->setEnabled(inner_box_enabled && field_is_active);
ui->lineEdit_ibox_maxZ->setEnabled(inner_box_enabled && field_is_active);
ui->lineEdit_ibox_minX->setEnabled(inner_box_enabled && field_is_active);
ui->lineEdit_ibox_minY->setEnabled(inner_box_enabled && field_is_active);
ui->lineEdit_ibox_minZ->setEnabled(inner_box_enabled && field_is_active);
ui->label_ibox_maxX->setEnabled(inner_box_enabled && field_is_active);
ui->label_ibox_maxY->setEnabled(inner_box_enabled && field_is_active);
ui->label_ibox_maxZ->setEnabled(inner_box_enabled && field_is_active);
ui->label_ibox_minX->setEnabled(inner_box_enabled && field_is_active);
ui->label_ibox_minY->setEnabled(inner_box_enabled && field_is_active);
ui->label_ibox_minZ->setEnabled(inner_box_enabled && field_is_active);
ui->radioButtonShip->setEnabled(!field_is_active);
ui->checkBoxBrown->setEnabled(debris_is_asteroid);
ui->checkBoxBlue->setEnabled(debris_is_asteroid);
ui->checkBoxOrange->setEnabled(debris_is_asteroid);
// speed text
ui->lineEditAvgSpeed->setText(_model->AsteroidEditorDialogModel::getAvgSpeed());
// ship debris comboboxes
for (auto i = 0; i < debrisComboBoxes.size(); ++i) {
debrisComboBoxes.at(i)->setCurrentIndex(_model->AsteroidEditorDialogModel::getFieldDebrisType(i));
debrisComboBoxes.at(i)->setEnabled(!debris_is_asteroid);
}
// mix/max field bounding boxes text
ui->lineEdit_obox_minX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MIN_X));
ui->lineEdit_obox_minY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MIN_Y));
ui->lineEdit_obox_minZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MIN_Z));
ui->lineEdit_obox_maxX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MAX_X));
ui->lineEdit_obox_maxY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MAX_Y));
ui->lineEdit_obox_maxZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MAX_Z));
ui->lineEdit_ibox_minX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MIN_X));
ui->lineEdit_ibox_minY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MIN_Y));
ui->lineEdit_ibox_minZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MIN_Z));
ui->lineEdit_ibox_maxX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MAX_X));
ui->lineEdit_ibox_maxY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MAX_Y));
ui->lineEdit_ibox_maxZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MAX_Z));
}
void AsteroidEditorDialog::done(int r)
{
if(QDialog::Accepted == r) // ok was pressed
{
// TODO consider moving the validation to when values are entered
// but just visually indicate that there's a problem at that time
// hard fail (by checking status boolean?) here instead
// i.e. let FREDers have temp bad values when they're changing stuff
// if they know what they're doing
if (_model->apply()) {
// all ok
QDialog::done(r);
_model->unset_modified();
return;
}
else {
// leave dialog open
return;
}
}
else // cancel, close or exc was pressed
{
if (_model->get_modified()) {
// give FREDer a chance in case they cancelled by mistake
// although I wonder if we're better off with always saving & don't prompt
// ~philisophical~
auto z = _viewport->dialogProvider->showButtonDialog(DialogType::Question,
"Question",
"You have unsaved changes, do you wish to discard them?",
{ DialogButton::Ok, DialogButton::Cancel });
if (z == DialogButton::Cancel) {
return;
}
}
QDialog::done(r);
_model->unset_modified();
return;
}
}
} // namespace dialogs
} // namespace fred
} // namespace fso
@@ -0,0 +1,69 @@
#pragma once
#include <QDialog>
#include <mission/dialogs/AsteroidEditorDialogModel.h>
#include <ui/FredView.h>
namespace fso {
namespace fred {
namespace dialogs {
namespace Ui {
class AsteroidEditorDialog;
}
class AsteroidEditorDialog : public QDialog
{
Q_OBJECT
public:
explicit AsteroidEditorDialog(FredView* parent, EditorViewport* viewport);
~AsteroidEditorDialog();
private:
void done(int r);
void toggleEnabled(bool enabled);
void toggleInnerBoxEnabled(bool enabled);
void toggleAsteroid(AsteroidEditorDialogModel::_roid_types colour, bool enabled);
void asteroidNumberChanged(int num_asteroids);
void setFieldActive();
void setFieldPassive();
void setGenreAsteroid();
void setGenreShip();
void changedBoxTextIMinX(const QString &text);
void changedBoxTextIMinY(const QString &text);
void changedBoxTextIMinZ(const QString &text);
void changedBoxTextIMaxX(const QString &text);
void changedBoxTextIMaxY(const QString &text);
void changedBoxTextIMaxZ(const QString &text);
void changedBoxTextOMinX(const QString &text);
void changedBoxTextOMinY(const QString &text);
void changedBoxTextOMinZ(const QString &text);
void changedBoxTextOMaxX(const QString &text);
void changedBoxTextOMaxY(const QString &text);
void changedBoxTextOMaxZ(const QString &text);
QString & getBoxText(AsteroidEditorDialogModel::_box_line_edits type);
void updateComboBox(int idx, int debris_type);
void updateUI();
EditorViewport* _viewport = nullptr;
Editor* _editor = nullptr;
std::unique_ptr<Ui::AsteroidEditorDialog> ui;
std::unique_ptr<AsteroidEditorDialogModel> _model;
QDoubleValidator _box_validator;
QIntValidator _speed_validator;
QList<QComboBox *> debrisComboBoxes;
};
} // namespace dialogs
} // namespace fred
} // namespace fso
+377
View File
@@ -0,0 +1,377 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>fso::fred::dialogs::AsteroidEditorDialog</class>
<widget class="QDialog" name="fso::fred::dialogs::AsteroidEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>675</width>
<height>339</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Debris Field Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QCheckBox" name="enabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="mainLayout">
<item>
<widget class="QGroupBox" name="fieldProperties">
<property name="enabled">
<bool>true</bool>
</property>
<property name="accessibleName">
<string>Field Properties</string>
</property>
<property name="title">
<string>Field Properties</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="2" column="0">
<widget class="QRadioButton" name="radioButtonActiveField">
<property name="text">
<string>Active Field</string>
</property>
<attribute name="buttonGroup">
<string notr="true">radioActivePassive</string>
</attribute>
</widget>
</item>
<item row="2" column="2">
<widget class="QRadioButton" name="radioButtonPassiveField">
<property name="text">
<string>Passive Field</string>
</property>
<attribute name="buttonGroup">
<string notr="true">radioActivePassive</string>
</attribute>
</widget>
</item>
<item row="4" column="0">
<widget class="QRadioButton" name="radioButtonAsteroid">
<property name="text">
<string>Asteroid</string>
</property>
<attribute name="buttonGroup">
<string notr="true">radioAsteroidShip</string>
</attribute>
</widget>
</item>
<item row="4" column="2">
<widget class="QRadioButton" name="radioButtonShip">
<property name="text">
<string>Ship</string>
</property>
<attribute name="buttonGroup">
<string notr="true">radioAsteroidShip</string>
</attribute>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="checkBoxBrown">
<property name="text">
<string>Brown</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="checkBoxBlue">
<property name="text">
<string>Blue</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QCheckBox" name="checkBoxOrange">
<property name="text">
<string>Orange</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QComboBox" name="debris0"/>
</item>
<item row="8" column="2">
<widget class="QComboBox" name="debris1"/>
</item>
<item row="10" column="2">
<widget class="QComboBox" name="debris2"/>
</item>
<item row="12" column="0">
<widget class="QLabel" name="labelNumber">
<property name="text">
<string>Number:</string>
</property>
</widget>
</item>
<item row="12" column="2">
<widget class="QSpinBox" name="spinBoxNumber"/>
</item>
<item row="13" column="0">
<widget class="QLabel" name="labelAvgSpeed">
<property name="text">
<string>Avg Speed:</string>
</property>
</widget>
</item>
<item row="13" column="2">
<widget class="QLineEdit" name="lineEditAvgSpeed"/>
</item>
<item row="1" column="0">
<spacer name="verticalSpacerFieldProperties">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="outerBox">
<property name="title">
<string>Outer Box</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QLabel" name="label_obox_minX">
<property name="text">
<string>Min X:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEdit_obox_minX"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_obox_maxX">
<property name="text">
<string>Max X:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_obox_maxX"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_obox_minY">
<property name="text">
<string>Min Y:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="lineEdit_obox_minY"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_obox_maxY">
<property name="text">
<string>Max Y:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="lineEdit_obox_maxY"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_obox_minZ">
<property name="text">
<string>Min Z:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="lineEdit_obox_minZ"/>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_obox_maxZ">
<property name="text">
<string>Max Z:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="lineEdit_obox_maxZ"/>
</item>
<item row="0" column="1">
<spacer name="verticalSpacerOuterBox">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="innerBox">
<property name="title">
<string>Inner Box</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="innerBoxEnabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_ibox_minX">
<property name="text">
<string>Min X:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="lineEdit_ibox_minX"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_ibox_maxX">
<property name="text">
<string>Max X:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEdit_ibox_maxX"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_ibox_minY">
<property name="text">
<string>Min Y:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="lineEdit_ibox_minY"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_ibox_maxY">
<property name="text">
<string>Max Y:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="lineEdit_ibox_maxY"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_ibox_minZ">
<property name="text">
<string>Min Z:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="lineEdit_ibox_minZ"/>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_ibox_maxZ">
<property name="text">
<string>Max Z:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="lineEdit_ibox_maxZ"/>
</item>
<item row="0" column="0">
<spacer name="verticalSpacerInnerBox">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QDialogButtonBox" name="dialogButtonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>dialogButtonBox</sender>
<signal>accepted()</signal>
<receiver>fso::fred::dialogs::AsteroidEditorDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>337</x>
<y>317</y>
</hint>
<hint type="destinationlabel">
<x>337</x>
<y>169</y>
</hint>
</hints>
</connection>
<connection>
<sender>dialogButtonBox</sender>
<signal>rejected()</signal>
<receiver>fso::fred::dialogs::AsteroidEditorDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>337</x>
<y>317</y>
</hint>
<hint type="destinationlabel">
<x>337</x>
<y>169</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="radioActivePassive"/>
<buttongroup name="radioAsteroidShip"/>
</buttongroups>
</ui>
+3
View File
@@ -1091,6 +1091,9 @@
<property name="text">
<string>&amp;Asteroid Field</string>
</property>
<property name="shortcut">
<string>Shift+A</string>
</property>
</action>
<action name="actionMission_Specs">
<property name="text">