diff --git dataobj/environment.cc dataobj/environment.cc index 4d9cc36316..ac237e6586 100644 --- dataobj/environment.cc +++ dataobj/environment.cc @@ -17,7 +17,7 @@ void rdwr_win_settings(loadsave_t *file); // simwin sint8 env_t::pak_tile_height_step = 16; sint8 env_t::pak_height_conversion_factor = 1; -bool env_t::new_height_map_conversion = false; +env_t::height_conversion_mode env_t::height_conv_mode = env_t::height_conversion_mode::linear; bool env_t::simple_drawing = false; bool env_t::simple_drawing_fast_forward = true; @@ -478,8 +478,18 @@ void env_t::rdwr(loadsave_t *file) file->rdwr_str( default_theme ); } if( file->is_version_atleast(120, 2) ) { - file->rdwr_bool( new_height_map_conversion ); + if( file->is_version_atleast(121, 1)) { + sint32 conv_mode = height_conv_mode; + file->rdwr_long( conv_mode ); + height_conv_mode = (env_t::height_conversion_mode)::clamp(conv_mode, 0, (int)env_t::height_conversion_mode::MAX_MODES); + } + else { + bool new_convert = height_conv_mode != height_conversion_mode::legacy_small; + file->rdwr_bool( new_convert ); + height_conv_mode = new_convert ? height_conversion_mode::legacy_large : height_conversion_mode::legacy_small; + } } + if( file->is_version_atleast(120, 5) ) { file->rdwr_long( background_color_rgb ); file->rdwr_long( tooltip_color_rgb ); diff --git dataobj/environment.h dataobj/environment.h index 02f8dcb129..d681ecf61f 100644 --- dataobj/environment.h +++ dataobj/environment.h @@ -325,8 +325,16 @@ public: /// Only use during loading of old games! static sint8 pak_height_conversion_factor; - // load old height maps (false) or use as many available height levels as possible - static bool new_height_map_conversion; + enum height_conversion_mode + { + legacy_small, ///< Old (fixed) height conversion, small height difference + legacy_large, ///< Old (fixed) height conversion, larger height difference + linear, ///< linear interpolation between min_/max_allowed_height + clamp, ///< Use 1 height level per 1 greyscale level, clamp to allowed height (cut off mountains) + MAX_MODES + }; + + static height_conversion_mode height_conv_mode; /// use the faster drawing routine (and allow for clipping errors) static bool simple_drawing; diff --git dataobj/height_map_loader.cc dataobj/height_map_loader.cc index 014eb3e5a1..7784cf00e9 100644 --- dataobj/height_map_loader.cc +++ dataobj/height_map_loader.cc @@ -3,246 +3,393 @@ * (see LICENSE.txt) */ -/* code for loading heightmaps */ - -#include -#include -#include +#include "height_map_loader.h" #include "environment.h" -#include "height_map_loader.h" + #include "../simio.h" #include "../sys/simsys.h" +#include "../simmem.h" +#include "../macros.h" + +#include +#include +#include -height_map_loader_t::height_map_loader_t(bool new_format): - height_map_conversion_version_new(new_format) - { +height_map_loader_t::height_map_loader_t(sint8 min_height, sint8 max_height, env_t::height_conversion_mode mode) : + min_allowed_height(min_height), + max_allowed_height(max_height), + conv_mode(mode) +{ } // read height data from bmp or ppm files bool height_map_loader_t::get_height_data_from_file( const char *filename, sint8 groundwater, sint8 *&hfield, sint16 &ww, sint16 &hh, bool update_only_values ) { - if (FILE* const file = dr_fopen(filename, "rb")) { - char id[3]; - // parsing the header of this mixed file format is nottrivial ... - id[0] = fgetc(file); - id[1] = fgetc(file); - id[2] = 0; - if(strcmp(id, "P6")) { - if(strcmp(id, "BM")) { - fclose(file); - dbg->error("height_map_loader_t::load_heightfield()","Heightfield has wrong image type %s instead P6/BM", id); - return false; + hfield = NULL; + + FILE *const file = dr_fopen(filename, "rb"); + if( !file ) { + dbg->error("height_map_loader_t::get_height_data_from_file()", "Cannot open heightmap file"); + return false; + } + + char id[3] = {0, 0, 0}; + // parsing the header of this mixed file format is non-trivial ... + if( fgets(id, 3, file) == NULL ) { + fclose(file); + dbg->error("height_map_loader_t::get_height_data_from_file()", "Cannot read magic number"); + return false; + } + + const bool is_bmp = strcmp(id, "BM") == 0; + const bool is_ppm = strcmp(id, "P6") == 0; + + if( !is_bmp && !is_ppm ) { + fclose(file); + dbg->error("height_map_loader_t::get_height_data_from_file()", "Unsupported height map format (must be bmp or ppm)"); + return false; + } + + const char *err = NULL; + if( is_bmp ) { + err = read_bmp(file, groundwater, hfield, ww, hh, update_only_values); + } + else { + err = read_ppm(file, groundwater, hfield, ww, hh, update_only_values); + } + + // success ... (or not) + fclose(file); + + if( err ) { + dbg->error("height_map_loader_t::get_height_data_from_file()", + "Error while reading %s file: %s", is_bmp ? "bmp" : "ppm", err); + + if( hfield ) { + free(hfield); + hfield = NULL; + } + } + + return err == NULL; +} + + +const char *height_map_loader_t::read_bmp( FILE *file, sint8 groundwater, sint8 *&hfield, sint16 &ww, sint16 &hh, bool update_only_values ) +{ + uint32 l; + uint16 s; + + if( fseek( file, 10, SEEK_SET ) != 0 || fread(&l, sizeof(uint32), 1, file) != 1 ) { + return "Malformed bmp file"; + } + const uint32 data_offset = endian(l); + + if( fseek( file, 18, SEEK_SET ) != 0 || fread(&l, sizeof(sint32), 1, file) != 1 ) { + return "Malformed bmp file"; + } + sint32 w = endian(l); + + if( fseek( file, 22, SEEK_SET ) != 0 || fread(&l, sizeof(sint32), 1, file) != 1 ) { + return "Malformed bmp file"; + } + sint32 h = endian(l); + + if( fseek( file, 28, SEEK_SET ) != 0 || fread(&s, sizeof(sint16), 1, file) != 1 ) { + return "Malformed bmp file"; + } + const sint16 bit_depth = endian(s); + + if( fseek( file, 30, SEEK_SET ) != 0 || fread(&l, sizeof(sint32), 1, file) != 1 ) { + return "Malformed bmp file"; + } + const sint32 format = endian(l); + + if( fseek( file, 46, SEEK_SET ) != 0 || fread(&l, sizeof(sint32), 1, file) != 1 ) { + return "Malformed bmp file"; + } + sint32 table = endian(l); + + if( (bit_depth!=8 && bit_depth!=24) || format>1 ) { + return "Can only use 8 bit (RLE or normal) or 24 bit bitmaps!"; + } + + // skip parsing body + if( update_only_values ) { + ww = w; + hh = abs(h); + return NULL; + } + + // now read the data and convert them on the fly + hfield = MALLOCN(sint8, w*h); + + if (!hfield) { + return "Not enough memory"; + } + + memset( hfield, groundwater, w*h ); + + if( bit_depth==8 ) { + // convert color tables to height levels + if( table==0 ) { + table = 256; + } + + sint8 h_table[256]; + if( fseek( file, 54, SEEK_SET ) != 0 ) { + return "Malformed bmp file"; + } + + for( int i=0; i1) { - if(!update_only_values) { - dbg->fatal("height_map_loader_t::get_height_data_from_file()","Can only use 8Bit (RLE or normal) or 24 bit bitmaps!"); + + h_table[i] = height_map_loader_t::rgb_to_height(R, G, B); + } + + // now read the data + if( fseek( file, data_offset, SEEK_SET ) != 0 ) { + return "Malformed bmp file"; + } + + if( format==0 ) { + // uncompressed (usually mirrored, if h<0) + const bool mirror = (h<0); + h = abs(h); + for( sint32 y=0; y0 ) { + for( sint32 k=0; k0) - bool mirror = (h<0); - h = abs(h); - for( sint32 y=0; y 0) { - for( sint32 k = 0; k < Count; k++, x++ ) { - hfield[x+(y*w)] = h_table[ColorIndex]; - } - } else if (Count == 0) { - sint32 Flag = ColorIndex; - if (Flag == 0) { - // goto next line - x = 0; - y--; - } - else if (Flag == 1) { - // end of bitmap - break; - } - else if (Flag == 2) { - // skip with cursor - x += (uint8)fgetc(file); - y -= (uint8)fgetc(file); - } - else { - // uncompressed run - Count = Flag; - for( sint32 k = 0; k < Count; k++, x++ ) { - hfield[x+y*w] = h_table[(uint8)fgetc(file)]; - } - if (ftell(file) & 1) { // always even offset in file - fseek(file, 1, SEEK_CUR); - } + else { + // uncompressed run + Count = Flag; + for( sint32 k=0; k='0' && *c<='9') { - c++; + } + else { + // uncompressed 24 bits + const bool mirror = (h<0); + h = abs(h); + + for( sint32 y=0; yfatal("height_map_loader_t::load_heightfield()","Heightfield has wrong color depth %d", param[2] ); - } - return false; + + // skip superfluous bytes at the end of each scanline + if( fseek( file, (4-((w*3)&3))&3, SEEK_CUR ) != 0 ) { + return "Malformed bmp file"; } + } + } + + ww = w; + hh = h; + return NULL; +} + + +const char *height_map_loader_t::read_ppm( FILE *file, sint8 groundwater, sint8 *&hfield, sint16 &ww, sint16 &hh, bool update_only_values ) +{ + // ppm format + char buf[255]; + const char *c = ""; + sint32 param[3] = {0, 0, 0}; + + for( int index=0; index<3; ) { + // the format is "P6[whitespace]width[whitespace]height[[whitespace bitdepth]]newline] + // however, Photoshop is the first program that uses space for the first whitespace + // so we cater for Photoshop too + while( *c!=0 && *c<=32 ) { + c++; + } - // report only values - if(update_only_values) { - fclose(file); - ww = w; - hh = h; - return true; + // usually, after P6 there comes a comment with the maker + // but comments can be anywhere + if( *c==0 ) { + if( read_line(buf, sizeof(buf), file) == NULL ) { + return "Malformed ppm file"; } - // ok, now read them in - hfield = new sint8[w*h]; - memset( hfield, groundwater, w*h ); + c = buf; + continue; + } - for(sint16 y=0; y='0' && *c<='9' ) { + c++; + } + } + + // now the data + const sint32 w = param[0]; + const sint32 h = param[1]; + + if( param[2]!=255 ) { + return "Heightfield has wrong color depth (must be 255)"; + } + + // report only values + if( update_only_values ) { + ww = w; + hh = h; + return NULL; + } + + // ok, now read them in + hfield = MALLOCN(sint8, w*h); + + if (!hfield) { + return "Not enough memory"; + } + + memset( hfield, groundwater, w*h ); + + for( sint16 y=0; yfatal("height_map_loader_t::rgb_to_height", "Unhandled height conversion mode %d", conv_mode); + } + + return 0; } diff --git dataobj/height_map_loader.h dataobj/height_map_loader.h index 4c4640e878..00c2539958 100644 --- dataobj/height_map_loader.h +++ dataobj/height_map_loader.h @@ -10,43 +10,39 @@ #include "../simtypes.h" #include "environment.h" -/* code for loading heightmaps */ -class height_map_loader_t { + +/** + * Loads height data from BMP or PPM files. + */ +class height_map_loader_t +{ public: - height_map_loader_t(bool height_map_conversion_version); + height_map_loader_t(sint8 min_allowed_height, sint8 max_allowed_height, env_t::height_conversion_mode conv_mode); /** - * Reads height data from 8 or 25 bit bmp or ppm files. - * @return Either pointer to heightfield (use delete [] for it) or NULL. + * Reads height data from 8 or 24 bit bmp or ppm files. + * + * @param filename the file to load the height data from. + * @param[out] hfield 2d array of height values. + * @param[out] ww,hh width and height of @p hfield. On failure, these values are undefined. + * @param update_only_values When true, do not allocate the height field; instead, only update @p ww and @p hh + * + * @return true on success, false on failure. On success, @p hfield contains + * the height field data (must be free()'d by the caller unless @p update_only_values is set). + * On failure, @p hfield is set to NULL (Does not need to be free()'d) */ bool get_height_data_from_file( const char *filename, sint8 groundwater, sint8 *&hfield, sint16 &ww, sint16 &hh, bool update_only_values ); private: - inline sint8 rgb_to_height( const int r, const int g, const int b ) { - const sint16 h0 = (r*2+g*3+b); - if( !height_map_conversion_version_new ) { - // old style - if( env_t::pak_height_conversion_factor == 2 ) { - // was: return (((r*2+g*3+b)/4 - 224) & 0xFFF8)/8; - return h0/32 - 28; - } - else { - // was: return (( ((r*2+g*3+b)/4 - 224)) & 0xFFF0)/16; - return h0/64 - 14; - } - } - else { - // new style, heights more spread out - if( env_t::pak_height_conversion_factor == 2 ) { - return h0/24 - 34; - } - else { - return h0/48 - 18; - } - } - } - - const bool height_map_conversion_version_new; + sint8 rgb_to_height( const int r, const int g, const int b ); + + const char *read_bmp(FILE *file, sint8 groundwater, sint8 *&hfield, sint16 &ww, sint16 &hh, bool update_only_values); + const char *read_ppm(FILE *file, sint8 groundwater, sint8 *&hfield, sint16 &ww, sint16 &hh, bool update_only_values); + +private: + const sint8 min_allowed_height; + const sint8 max_allowed_height; + const env_t::height_conversion_mode conv_mode; }; #endif diff --git dataobj/settings.cc dataobj/settings.cc index 307d2ad019..eb7ac6b559 100644 --- dataobj/settings.cc +++ dataobj/settings.cc @@ -1319,7 +1319,8 @@ void settings_t::parse_simuconf(tabfile_t& simuconf, sint16& disp_width, sint16& starting_year = contents.get_int("starting_year", starting_year ); starting_month = contents.get_int("starting_month", starting_month+1)-1; - env_t::new_height_map_conversion = contents.get_int("new_height_map_conversion", env_t::new_height_map_conversion ); + env_t::height_conv_mode = (env_t::height_conversion_mode)::clamp(contents.get_int("new_height_map_conversion", (int)env_t::height_conv_mode ), 0, env_t::height_conversion_mode::MAX_MODES); + river_number = contents.get_int("river_number", river_number ); min_river_length = contents.get_int("river_min_length", min_river_length ); max_river_length = contents.get_int("river_max_length", max_river_length ); @@ -1484,13 +1485,10 @@ void settings_t::parse_simuconf(tabfile_t& simuconf, sint16& disp_width, sint16& max_rail_convoi_length = contents.get_int("max_rail_convoi_length",max_rail_convoi_length); max_road_convoi_length = contents.get_int("max_road_convoi_length",max_road_convoi_length); max_ship_convoi_length = contents.get_int("max_ship_convoi_length",max_ship_convoi_length); - max_air_convoi_length = contents.get_int("max_air_convoi_length",max_air_convoi_length); + max_air_convoi_length = contents.get_int("max_air_convoi_length",max_air_convoi_length); - world_maximum_height = contents.get_int("world_maximum_height",world_maximum_height); - world_minimum_height = contents.get_int("world_minimum_height",world_minimum_height); - if( world_minimum_height>=world_maximum_height ) { - world_minimum_height = world_maximum_height-1; - } + world_maximum_height = clamp(contents.get_int("world_maximum_height",world_maximum_height), 16, 127); + world_minimum_height = clamp(contents.get_int("world_minimum_height",world_minimum_height), -127, -12); // Default pak file path objfilename = ltrim(contents.get_string("pak_file_path", "" ) ); diff --git gui/load_relief_frame.cc gui/load_relief_frame.cc index 5dd7618846..9a1901efaf 100644 --- gui/load_relief_frame.cc +++ gui/load_relief_frame.cc @@ -14,6 +14,32 @@ #include "../dataobj/settings.h" #include "../dataobj/environment.h" #include "../dataobj/height_map_loader.h" +#include "components/gui_scrolled_list.h" + + +static const char *load_mode_texts[env_t::MAX_MODES] = +{ + "legacy (small heights)", + "legacy (large heights)", + "linear", + "clamp" +}; + + +class load_relief_mode_scrollitem_t : public gui_scrolled_list_t::const_text_scrollitem_t +{ +public: + load_relief_mode_scrollitem_t(env_t::height_conversion_mode mode) : + gui_scrolled_list_t::const_text_scrollitem_t(translator::translate(load_mode_texts[(int)mode]), SYSCOL_TEXT), + mode(mode) + {} + + env_t::height_conversion_mode get_mode() const { return mode; } + +private: + env_t::height_conversion_mode mode; +}; + /** * Action, started on button pressing @@ -23,17 +49,33 @@ bool load_relief_frame_t::item_action(const char *fullpath) sets->heightfield = fullpath; if (gui_frame_t *new_world_gui = win_get_magic( magic_welt_gui_t )) { + const gui_scrolled_list_t::scrollitem_t *selected = load_mode.get_selected_item(); + if (selected) { + env_t::height_conv_mode = static_cast(selected)->get_mode(); + } + static_cast(new_world_gui)->update_preview(true); } + return false; } load_relief_frame_t::load_relief_frame_t(settings_t* const sets) : savegame_frame_t( NULL, false, "maps/", env_t::show_delete_buttons ) { - new_format.init( button_t::square_automatic, "Maximize height levels"); - new_format.pressed = env_t::new_height_map_conversion; - bottom_left_frame.add_component( &new_format ); + gui_aligned_container_t *table = bottom_left_frame.add_table(2, 1); + load_mode_label.init(translator::translate("Load mode:"), scr_coord(0, 0)); + load_mode_label.set_visible(true); + table->add_component( &load_mode_label ); + + load_mode.init(scr_coord(0, 0)); + load_mode.new_component(env_t::height_conversion_mode::legacy_small); + load_mode.new_component(env_t::height_conversion_mode::legacy_large); + load_mode.new_component(env_t::height_conversion_mode::linear); + load_mode.new_component(env_t::height_conversion_mode::clamp); + load_mode.set_selection(env_t::height_conversion_mode::linear); + table->add_component( &load_mode ); + bottom_left_frame.end_table(); const std::string extra_path = env_t::program_dir + env_t::objfilename + "maps/"; this->add_path(extra_path.c_str()); @@ -49,27 +91,48 @@ const char *load_relief_frame_t::get_info(const char *fullpath) static char size[64]; sint16 w, h; - sint8 *h_field ; - height_map_loader_t hml(new_format.pressed); + sint8 *h_field = NULL; + const sint8 min_h = world()->get_settings().get_minimumheight(); + const sint8 max_h = world()->get_settings().get_maximumheight(); + + const gui_scrolled_list_t::scrollitem_t *selected = load_mode.get_selected_item(); + env_t::height_conversion_mode new_mode = env_t::height_conv_mode; + if (selected) { + new_mode = static_cast(selected)->get_mode(); + } + + height_map_loader_t hml(min_h, max_h, env_t::height_conv_mode); if(hml.get_height_data_from_file(fullpath, (sint8)sets->get_groundwater(), h_field, w, h, true )) { sprintf( size, "%i x %i", w, h ); - env_t::new_height_map_conversion = new_format.pressed; + env_t::height_conv_mode = new_mode; return size; } + return ""; } bool load_relief_frame_t::check_file( const char *fullpath, const char * ) { + const sint8 min_h = world()->get_settings().get_minimumheight(); + const sint8 max_h = world()->get_settings().get_maximumheight(); + + const gui_scrolled_list_t::scrollitem_t *selected = load_mode.get_selected_item(); + env_t::height_conversion_mode new_mode = env_t::height_conv_mode; + + if (selected) { + new_mode = static_cast(selected)->get_mode(); + } + + height_map_loader_t hml(min_h, max_h, env_t::height_conv_mode); sint16 w, h; - sint8 *h_field; + sint8 *h_field = NULL; - height_map_loader_t hml(new_format.pressed); if(hml.get_height_data_from_file(fullpath, (sint8)sets->get_groundwater(), h_field, w, h, true )) { + env_t::height_conv_mode = new_mode; return w>0 && h>0; - env_t::new_height_map_conversion = new_format.pressed; } + return false; } diff --git gui/load_relief_frame.h gui/load_relief_frame.h index 971df2c0cc..79b79d93b8 100644 --- gui/load_relief_frame.h +++ gui/load_relief_frame.h @@ -7,17 +7,20 @@ #define GUI_LOAD_RELIEF_FRAME_H +#include "components/gui_combobox.h" #include "savegame_frame.h" class settings_t; + class load_relief_frame_t : public savegame_frame_t { private: settings_t* sets; - button_t new_format; + gui_label_t load_mode_label; + gui_combobox_t load_mode; protected: bool item_action(const char *fullpath) OVERRIDE; diff --git gui/loadfont_frame.cc gui/loadfont_frame.cc index 6f06878ade..4a4be1ba2e 100644 --- gui/loadfont_frame.cc +++ gui/loadfont_frame.cc @@ -253,7 +253,6 @@ void loadfont_frame_t::rdwr( loadsave_t *file ) } - bool loadfont_frame_t::action_triggered(gui_action_creator_t *component, value_t v) { if( &unicode_only==component ) { diff --git gui/welt.cc gui/welt.cc index b76f161b4b..ff307442e3 100644 --- gui/welt.cc +++ gui/welt.cc @@ -274,9 +274,13 @@ bool welt_gui_t::update_from_heightfield(const char *filename) { DBG_MESSAGE("welt_gui_t::update_from_heightfield()", "%s", filename); + const sint8 min_h = env_t::default_settings.get_minimumheight(); + const sint8 max_h = env_t::default_settings.get_maximumheight(); + + height_map_loader_t hml(min_h, max_h, env_t::height_conv_mode); sint16 w, h; sint8 *h_field=NULL; - height_map_loader_t hml(sets); + if(hml.get_height_data_from_file(filename, (sint8)sets->get_groundwater(), h_field, w, h, false )) { sets->set_size_x(w); sets->set_size_y(h); @@ -295,8 +299,10 @@ bool welt_gui_t::update_from_heightfield(const char *filename) } } map_preview.set_map_data(&map); + free(h_field); return true; } + return false; } diff --git simworld.cc simworld.cc index bf8213cbe5..c06df69d9b 100644 --- simworld.cc +++ simworld.cc @@ -6274,13 +6274,17 @@ uint8 karte_t::sp2num(player_t *player) void karte_t::load_heightfield(settings_t* const sets) { sint16 w, h; - sint8 *h_field; - height_map_loader_t hml(sets); + sint8 *h_field = NULL; + const sint8 min_h = sets->get_minimumheight(); + const sint8 max_h = sets->get_maximumheight(); + + height_map_loader_t hml(min_h, max_h, env_t::height_conv_mode); + if(hml.get_height_data_from_file(sets->heightfield.c_str(), (sint8)(sets->get_groundwater()), h_field, w, h, false )) { sets->set_size(w,h); // create map init(sets,h_field); - delete [] h_field; + free(h_field); } else { dbg->error("karte_t::load_heightfield()","Cant open file '%s'", sets->heightfield.c_str());