mirror of https://github.com/OpenTTD/OpenTTD
Feature: Import town data from JSON file (#10409)
parent
908ee7292b
commit
ad020759c7
|
@ -0,0 +1,97 @@
|
|||
# Importing Town Data into OpenTTD
|
||||
|
||||
To aid players in scenario creation, OpenTTD's Scenario Editor can import town data from external JSON files. This enables players to use an image editing program to align town coordinates with a real-world heightmap using a map underlay, instead of guessing at the correct locations in Scenario Editor itself.
|
||||
|
||||
This town data consists of a JSON file storing an array of town data objects, each containing a name, location, target OpenTTD population, and whether it is marked as a city in the game.
|
||||
|
||||
This document describes the standard format for this JSON file and outlines a workflow for creating this data effectively.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- Why load external data?
|
||||
- How to use this feature
|
||||
- Creating geodata
|
||||
- Town data format standards
|
||||
- Town data values
|
||||
- Loading geodata into OpenTTD
|
||||
- Tutorial: Creating town data
|
||||
|
||||
## Why load external data?
|
||||
|
||||
There are three benefits to using an image editing program to create towns instead of the OpenTTD Scenario Editor.
|
||||
|
||||
1. Placing towns accurately is much easier using a map underlay such as OpenStreetMap to match town locations with the corresponding heightmap.
|
||||
2. Storing town data in a JSON file instead of as an OpenTTD Scenario (.scn) doesn't require choosing your NewGRF house set before placing towns.
|
||||
3. Town coordinates are scaled by the map size, so you can load the data onto whatever size map you like.
|
||||
|
||||
## How to use this feature
|
||||
|
||||
### Creating geodata
|
||||
|
||||
Town data is a text file in the JSON format, with a list of towns, each containing a coordinate location and properties: name, population, and whether or not it should be a city in OpenTTD.
|
||||
|
||||
The format of this file is standardized for importing into OpenTTD and must be followed for OpenTTD to properly parse the data.
|
||||
|
||||
For use in OpenTTD, you will also need a matching heightmap of the terrain features, as a PNG.
|
||||
|
||||
#### Town data format standards
|
||||
|
||||
The following code sample is complete and can be used in OpenTTD.
|
||||
- The list of towns is enclosed in an array marked with square brackets `[]`
|
||||
- Each town is enclosed in curly braces `{}`, with a comma after each town except for the last in the list.
|
||||
- The properties separated by commas except for the last.
|
||||
- Property names are enclosed in double quotes `""` with a colon `:` separating it from the property value.
|
||||
- The name property value is enclosed in double quotes `"London"`, while all other property values `44910`, `true`, etc., are not.
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"name": "London",
|
||||
"population": 44910,
|
||||
"city": true,
|
||||
"x": 0.7998046875,
|
||||
"y": 0.708984375
|
||||
},
|
||||
{
|
||||
"name": "Canterbury",
|
||||
"population": 217.16,
|
||||
"city": false,
|
||||
"x": 0.83251953125,
|
||||
"y": 0.828125
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Town data values
|
||||
|
||||
- Population is scaled down for use in OpenTTD. It is possible to generate huge cities by using a large number, but there is a practical limit to town size. The larger the town, the longer it will take to import town data, since towns are placed at a relatively small size and then expanded until the population is greater than the player-defined target.
|
||||
- X and Y coordinates are a proportion of the total map dimension, between 0 and 1. Just take the pixel coordinates of the town's location in the corresponding heightmap (more detail in the tutorial below) and divide each by the maximum value.
|
||||
- For example, London is at `726, 1638` in a 1024 px by 2048 px heightmap, so `726 / 1024 = 0.7998046875` and `1638 / 2048 = 0.708984375` gives the correct coordinates for OpenTTD.
|
||||
- The reason for these proportional coordinates is so the data can be used for any map size.
|
||||
- 0,0 is (approximately) the very top tile in OpenTTD. You can see tile coordinates in-game with the Land Info Tool.
|
||||
- In most image editing programs, 0,0 is the top-left corner of the image. You can rotate the image however you want relative to compass north to orient the map to your liking. Make sure you crop and resize the image before recording town locations.
|
||||
- In OpenTTD, X and Y axis are swapped compared to most image editing programs and the standard Cartesian coordinate system. From the 0,0 origin at top left, X is the axis along the left side and Y is the axis along the right side. You can still measure X and Y coordinates in your image editing program, just swap them before importing into OpenTTD or towns won't line up with your heightmap.
|
||||
|
||||
### Loading geodata into OpenTTD
|
||||
Using geodata to create a real-world location in OpenTTD is done in the Scenario Editor.
|
||||
|
||||
1. Choose the NewGRFs you want to use in the game.
|
||||
2. Load the heightmap which you created in the geodata workflow. Either rotation will work, but the clockwise rotation is considered "correct" and the coordinates in the Land Info Tool will match your data; counter clockwise maps will align properly but the coordinates won't match your data.
|
||||
3. In the Town Generation window, click `Load from file` and choose the .json file containing town data. The default directory to search for town data is `OpenTTD\scenario\heightmap`.
|
||||
4. (Optional) Manually add industries, rivers, trees, and objects.
|
||||
5. Save the game as a Scenario and exit to the main menu.
|
||||
6. Load the game with Play Scenario and enjoy.
|
||||
|
||||
Sometimes it's not possible to place a town, such as when the heightmap is very rough and a flat tile can't be found with a 16-tile radius of the target tile. In such cases, a sign will be placed on the target tile with the name of the town. The player can then place the town manually or change the heightmap settings and try again. This fallback also helps debug errors with data creation, such as if towns end up in the ocean.
|
||||
|
||||
## Tutorial: Creating town data
|
||||
|
||||
1. Load both your heightmap and a labeled map like OpenStreetMap as layers in an image editing program. You can use a free/open-source program like QGIS to acquire, align, and export these map images, if you like.
|
||||
2. Crop the image to your desired bounds, ensuring the aspect ratio is supported in OpenTTD (1:1, 1:2, 1:4, etc.).
|
||||
3. Resize the image to one of OpenTTD's supported map sizes, such as 512 px by 1024 px. Some image editors let you do this part of step 2. You can always load heightmaps and town data at a reduced size, so you may want to make this larger than your intended use in case you want it later.
|
||||
4. Use the labeled map layer to find the pixel coordinates of each town you'd like to include in your map. In GIMP this is displayed in the bottom left corner of the image window, and in Photoshop you need to enable the Info panel (F8) and switch to pixel units of measurement if not already.
|
||||
5. Some spreadsheets including Google Sheets can export data as JSON, so you may want to record it there, to export after step 8. Or you can build the JSON file manually.
|
||||
6. Adjust population numbers for OpenTTD.
|
||||
7. Change coordinates from pixels to proportion (0-1) of the total dimension: `x / maximum_x` and `y / maximum_y`, as described in "Town data values" above.
|
||||
8. Swap X and Y coordinates before importing to OpenTTD, since OpenTTD uses a reverse X and Y system than most image editors.
|
||||
9. Save the heightmap and town data files in your `OpenTTD\scenario\heightmap` folder.
|
|
@ -18,6 +18,7 @@ enum AbstractFileType {
|
|||
FT_SAVEGAME, ///< old or new savegame
|
||||
FT_SCENARIO, ///< old or new scenario
|
||||
FT_HEIGHTMAP, ///< heightmap file
|
||||
FT_TOWN_DATA, ///< town data file
|
||||
|
||||
FT_INVALID = 7, ///< Invalid or unknown file type.
|
||||
FT_NUMBITS = 3, ///< Number of bits required for storing a #AbstractFileType value.
|
||||
|
@ -34,6 +35,9 @@ enum DetailedFileType {
|
|||
DFT_HEIGHTMAP_BMP, ///< BMP file.
|
||||
DFT_HEIGHTMAP_PNG, ///< PNG file.
|
||||
|
||||
/* Town data files. */
|
||||
DFT_TOWN_DATA_JSON, ///< JSON file.
|
||||
|
||||
/* fios 'files' */
|
||||
DFT_FIOS_DRIVE, ///< A drive (letter) entry.
|
||||
DFT_FIOS_PARENT, ///< A parent directory entry.
|
||||
|
@ -78,6 +82,7 @@ enum FiosType {
|
|||
FIOS_TYPE_OLD_SCENARIO = MAKE_FIOS_TYPE(FT_SCENARIO, DFT_OLD_GAME_FILE),
|
||||
FIOS_TYPE_PNG = MAKE_FIOS_TYPE(FT_HEIGHTMAP, DFT_HEIGHTMAP_PNG),
|
||||
FIOS_TYPE_BMP = MAKE_FIOS_TYPE(FT_HEIGHTMAP, DFT_HEIGHTMAP_BMP),
|
||||
FIOS_TYPE_JSON = MAKE_FIOS_TYPE(FT_TOWN_DATA, DFT_TOWN_DATA_JSON),
|
||||
|
||||
FIOS_TYPE_INVALID = MAKE_FIOS_TYPE(FT_INVALID, DFT_INVALID),
|
||||
};
|
||||
|
|
41
src/fios.cpp
41
src/fios.cpp
|
@ -84,6 +84,10 @@ void FileList::BuildFileList(AbstractFileType abstract_filetype, SaveLoadOperati
|
|||
FiosGetHeightmapList(fop, show_dirs, *this);
|
||||
break;
|
||||
|
||||
case FT_TOWN_DATA:
|
||||
FiosGetTownDataList(fop, show_dirs, *this);
|
||||
break;
|
||||
|
||||
default:
|
||||
NOT_REACHED();
|
||||
}
|
||||
|
@ -180,6 +184,7 @@ bool FiosBrowseTo(const FiosItem *item)
|
|||
case FIOS_TYPE_OLD_SCENARIO:
|
||||
case FIOS_TYPE_PNG:
|
||||
case FIOS_TYPE_BMP:
|
||||
case FIOS_TYPE_JSON:
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -553,6 +558,42 @@ void FiosGetHeightmapList(SaveLoadOperation fop, bool show_dirs, FileList &file_
|
|||
FiosGetFileList(fop, show_dirs, &FiosGetHeightmapListCallback, subdir, file_list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for FiosGetTownDataList.
|
||||
* @param fop Purpose of collecting the list.
|
||||
* @param file Name of the file to check.
|
||||
* @return a FIOS_TYPE_JSON type of the found file, FIOS_TYPE_INVALID if not a valid JSON file, and the title of the file (if any).
|
||||
*/
|
||||
static std::tuple<FiosType, std::string> FiosGetTownDataListCallback(SaveLoadOperation fop, const std::string &file, const std::string_view ext)
|
||||
{
|
||||
if (fop == SLO_LOAD) {
|
||||
if (StrEqualsIgnoreCase(ext, ".json")) {
|
||||
return { FIOS_TYPE_JSON, GetFileTitle(file, SAVE_DIR) };
|
||||
}
|
||||
}
|
||||
|
||||
return { FIOS_TYPE_INVALID, {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of town data files.
|
||||
* @param fop Purpose of collecting the list.
|
||||
* @param show_dirs Whether to show directories.
|
||||
* @param file_list Destination of the found files.
|
||||
*/
|
||||
void FiosGetTownDataList(SaveLoadOperation fop, bool show_dirs, FileList &file_list)
|
||||
{
|
||||
static std::optional<std::string> fios_town_data_path;
|
||||
|
||||
if (!fios_town_data_path) fios_town_data_path = FioFindDirectory(HEIGHTMAP_DIR);
|
||||
|
||||
_fios_path = &(*fios_town_data_path);
|
||||
|
||||
std::string base_path = FioFindDirectory(HEIGHTMAP_DIR);
|
||||
Subdirectory subdir = base_path == *_fios_path ? HEIGHTMAP_DIR : NO_DIRECTORY;
|
||||
FiosGetFileList(fop, show_dirs, &FiosGetTownDataListCallback, subdir, file_list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for screenshots.
|
||||
* @return path to screenshots
|
||||
|
|
|
@ -107,6 +107,7 @@ void ShowSaveLoadDialog(AbstractFileType abstract_filetype, SaveLoadOperation fo
|
|||
void FiosGetSavegameList(SaveLoadOperation fop, bool show_dirs, FileList &file_list);
|
||||
void FiosGetScenarioList(SaveLoadOperation fop, bool show_dirs, FileList &file_list);
|
||||
void FiosGetHeightmapList(SaveLoadOperation fop, bool show_dirs, FileList &file_list);
|
||||
void FiosGetTownDataList(SaveLoadOperation fop, bool show_dirs, FileList &file_list);
|
||||
|
||||
bool FiosBrowseTo(const FiosItem *item);
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include "querystring_gui.h"
|
||||
#include "engine_func.h"
|
||||
#include "landscape_type.h"
|
||||
#include "genworld.h"
|
||||
#include "timer/timer_game_calendar.h"
|
||||
#include "core/geometry_func.hpp"
|
||||
#include "gamelog.h"
|
||||
|
@ -172,6 +173,48 @@ static constexpr NWidgetPart _nested_load_heightmap_dialog_widgets[] = {
|
|||
EndContainer(),
|
||||
};
|
||||
|
||||
/** Load town data */
|
||||
static constexpr NWidgetPart _nested_load_town_data_dialog_widgets[] = {
|
||||
NWidget(NWID_HORIZONTAL),
|
||||
NWidget(WWT_CLOSEBOX, COLOUR_GREY),
|
||||
NWidget(WWT_CAPTION, COLOUR_GREY, WID_SL_CAPTION),
|
||||
NWidget(WWT_DEFSIZEBOX, COLOUR_GREY),
|
||||
EndContainer(),
|
||||
/* Current directory and free space */
|
||||
NWidget(WWT_PANEL, COLOUR_GREY, WID_SL_BACKGROUND), SetFill(1, 0), SetResize(1, 0), EndContainer(),
|
||||
|
||||
/* Filter box with label */
|
||||
NWidget(WWT_PANEL, COLOUR_GREY), SetFill(1, 1), SetResize(1, 1),
|
||||
NWidget(NWID_HORIZONTAL), SetPadding(WidgetDimensions::unscaled.framerect.top, 0, WidgetDimensions::unscaled.framerect.bottom, 0),
|
||||
SetPIP(WidgetDimensions::unscaled.frametext.left, WidgetDimensions::unscaled.frametext.right, 0),
|
||||
NWidget(WWT_TEXT, COLOUR_GREY), SetFill(0, 1), SetDataTip(STR_SAVELOAD_FILTER_TITLE , STR_NULL),
|
||||
NWidget(WWT_EDITBOX, COLOUR_GREY, WID_SL_FILTER), SetFill(1, 0), SetResize(1, 0), SetDataTip(STR_LIST_FILTER_OSKTITLE, STR_LIST_FILTER_TOOLTIP),
|
||||
EndContainer(),
|
||||
EndContainer(),
|
||||
/* Sort Buttons */
|
||||
NWidget(NWID_HORIZONTAL),
|
||||
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SL_SORT_BYNAME), SetDataTip(STR_SORT_BY_CAPTION_NAME, STR_TOOLTIP_SORT_ORDER), SetFill(1, 0), SetResize(1, 0),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SL_SORT_BYDATE), SetDataTip(STR_SORT_BY_CAPTION_DATE, STR_TOOLTIP_SORT_ORDER), SetFill(1, 0), SetResize(1, 0),
|
||||
EndContainer(),
|
||||
NWidget(WWT_PUSHIMGBTN, COLOUR_GREY, WID_SL_HOME_BUTTON), SetAspect(1), SetDataTip(SPR_HOUSE_ICON, STR_SAVELOAD_HOME_BUTTON),
|
||||
EndContainer(),
|
||||
/* Files */
|
||||
NWidget(NWID_HORIZONTAL),
|
||||
NWidget(WWT_PANEL, COLOUR_GREY, WID_SL_FILE_BACKGROUND),
|
||||
NWidget(WWT_INSET, COLOUR_GREY, WID_SL_DRIVES_DIRECTORIES_LIST), SetFill(1, 1), SetPadding(2, 2, 2, 2),
|
||||
SetDataTip(0x0, STR_SAVELOAD_LIST_TOOLTIP), SetResize(1, 10), SetScrollbar(WID_SL_SCROLLBAR), EndContainer(),
|
||||
EndContainer(),
|
||||
NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_SL_SCROLLBAR),
|
||||
EndContainer(),
|
||||
/* Load button */
|
||||
NWidget(NWID_HORIZONTAL, NC_EQUALSIZE),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_SL_LOAD_BUTTON), SetResize(1, 0), SetFill(1, 0),
|
||||
SetDataTip(STR_SAVELOAD_LOAD_BUTTON, STR_SAVELOAD_LOAD_TOWN_DATA_TOOLTIP),
|
||||
NWidget(WWT_RESIZEBOX, COLOUR_GREY),
|
||||
EndContainer(),
|
||||
};
|
||||
|
||||
/** Save game/scenario */
|
||||
static constexpr NWidgetPart _nested_save_dialog_widgets[] = {
|
||||
NWidget(NWID_HORIZONTAL),
|
||||
|
@ -242,6 +285,7 @@ static const TextColour _fios_colours[] = {
|
|||
TC_ORANGE, // DFT_GAME_FILE
|
||||
TC_YELLOW, // DFT_HEIGHTMAP_BMP
|
||||
TC_ORANGE, // DFT_HEIGHTMAP_PNG
|
||||
TC_LIGHT_BROWN, // DFT_TOWN_DATA_JSON
|
||||
TC_LIGHT_BLUE, // DFT_FIOS_DRIVE
|
||||
TC_DARK_GREEN, // DFT_FIOS_PARENT
|
||||
TC_DARK_GREEN, // DFT_FIOS_DIR
|
||||
|
@ -330,6 +374,7 @@ public:
|
|||
break;
|
||||
|
||||
default:
|
||||
/* It's not currently possible to save town data. */
|
||||
NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
@ -356,6 +401,10 @@ public:
|
|||
caption_string = (this->fop == SLO_SAVE) ? STR_SAVELOAD_SAVE_HEIGHTMAP : STR_SAVELOAD_LOAD_HEIGHTMAP;
|
||||
break;
|
||||
|
||||
case FT_TOWN_DATA:
|
||||
caption_string = STR_SAVELOAD_LOAD_TOWN_DATA; // It's not currently possible to save town data.
|
||||
break;
|
||||
|
||||
default:
|
||||
NOT_REACHED();
|
||||
}
|
||||
|
@ -391,6 +440,7 @@ public:
|
|||
break;
|
||||
|
||||
case FT_HEIGHTMAP:
|
||||
case FT_TOWN_DATA:
|
||||
o_dir.name = FioFindDirectory(HEIGHTMAP_DIR);
|
||||
break;
|
||||
|
||||
|
@ -634,6 +684,9 @@ public:
|
|||
if (this->abstract_filetype == FT_HEIGHTMAP) {
|
||||
this->Close();
|
||||
ShowHeightmapLoad();
|
||||
} else if (this->abstract_filetype == FT_TOWN_DATA) {
|
||||
this->Close();
|
||||
LoadTownData();
|
||||
} else if (!_load_check_data.HasNewGrfs() || _load_check_data.grf_compatibility != GLC_NOT_FOUND || _settings_client.gui.UserIsAllowedToChangeNewGRFs()) {
|
||||
_switch_mode = (_game_mode == GM_EDITOR) ? SM_LOAD_SCENARIO : SM_LOAD_GAME;
|
||||
ClearErrorMessages();
|
||||
|
@ -689,7 +742,7 @@ public:
|
|||
} else if (!_load_check_data.HasErrors()) {
|
||||
this->selected = file;
|
||||
if (this->fop == SLO_LOAD) {
|
||||
if (this->abstract_filetype == FT_SAVEGAME || this->abstract_filetype == FT_SCENARIO) {
|
||||
if (this->abstract_filetype == FT_SAVEGAME || this->abstract_filetype == FT_SCENARIO || this->abstract_filetype == FT_TOWN_DATA) {
|
||||
this->OnClick(pt, WID_SL_LOAD_BUTTON, 1);
|
||||
} else {
|
||||
assert(this->abstract_filetype == FT_HEIGHTMAP);
|
||||
|
@ -856,6 +909,7 @@ public:
|
|||
|
||||
switch (this->abstract_filetype) {
|
||||
case FT_HEIGHTMAP:
|
||||
case FT_TOWN_DATA:
|
||||
this->SetWidgetDisabledState(WID_SL_LOAD_BUTTON, this->selected == nullptr || _load_check_data.HasErrors());
|
||||
break;
|
||||
|
||||
|
@ -908,6 +962,14 @@ static WindowDesc _load_heightmap_dialog_desc(
|
|||
_nested_load_heightmap_dialog_widgets
|
||||
);
|
||||
|
||||
/** Load town data */
|
||||
static WindowDesc _load_town_data_dialog_desc(
|
||||
WDP_CENTER, "load_town_data", 257, 320,
|
||||
WC_SAVELOAD, WC_NONE,
|
||||
0,
|
||||
_nested_load_town_data_dialog_widgets
|
||||
);
|
||||
|
||||
/** Save game/scenario */
|
||||
static WindowDesc _save_dialog_desc(
|
||||
WDP_CENTER, "save_game", 500, 294,
|
||||
|
@ -929,6 +991,17 @@ void ShowSaveLoadDialog(AbstractFileType abstract_filetype, SaveLoadOperation fo
|
|||
new SaveLoadWindow(_save_dialog_desc, abstract_filetype, fop);
|
||||
} else {
|
||||
/* Dialogue for loading a file. */
|
||||
new SaveLoadWindow((abstract_filetype == FT_HEIGHTMAP) ? _load_heightmap_dialog_desc : _load_dialog_desc, abstract_filetype, fop);
|
||||
switch (abstract_filetype) {
|
||||
case FT_HEIGHTMAP:
|
||||
new SaveLoadWindow(_load_heightmap_dialog_desc, abstract_filetype, fop);
|
||||
break;
|
||||
|
||||
case FT_TOWN_DATA:
|
||||
new SaveLoadWindow(_load_town_data_dialog_desc, abstract_filetype, fop);
|
||||
break;
|
||||
|
||||
default:
|
||||
new SaveLoadWindow(_load_dialog_desc, abstract_filetype, fop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
179
src/genworld.cpp
179
src/genworld.cpp
|
@ -10,6 +10,10 @@
|
|||
#include "stdafx.h"
|
||||
#include "landscape.h"
|
||||
#include "company_func.h"
|
||||
#include "town_cmd.h"
|
||||
#include "signs_cmd.h"
|
||||
#include "3rdparty/nlohmann/json.hpp"
|
||||
#include "strings_func.h"
|
||||
#include "genworld.h"
|
||||
#include "gfxinit.h"
|
||||
#include "window_func.h"
|
||||
|
@ -337,3 +341,178 @@ void GenerateWorld(GenWorldMode mode, uint size_x, uint size_y, bool reset_setti
|
|||
|
||||
_GenerateWorld();
|
||||
}
|
||||
|
||||
/** Town data imported from JSON files and used to place towns. */
|
||||
struct ExternalTownData {
|
||||
TownID town_id; ///< The TownID of the town in OpenTTD. Not imported, but set during the founding proceess and stored here for convenience.
|
||||
std::string name; ///< The name of the town.
|
||||
uint population; ///< The target population of the town when created in OpenTTD. If input is blank, defaults to 0.
|
||||
bool is_city; ///< Should it be created as a city in OpenTTD? If input is blank, defaults to false.
|
||||
float x_proportion; ///< The X coordinate of the town, as a proportion 0..1 of the maximum X coordinate.
|
||||
float y_proportion; ///< The Y coordinate of the town, as a proportion 0..1 of the maximum Y coordinate.
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper for CircularTileSearch to found a town on or near a given tile.
|
||||
* @param tile The tile to try founding the town upon.
|
||||
* @param user_data The ExternalTownData to attempt to found.
|
||||
* @return True if the town was founded successfully.
|
||||
*/
|
||||
static bool TryFoundTownNearby(TileIndex tile, void *user_data)
|
||||
{
|
||||
ExternalTownData &town = *static_cast<ExternalTownData *>(user_data);
|
||||
std::tuple<CommandCost, Money, TownID> result = Command<CMD_FOUND_TOWN>::Do(DC_EXEC, tile, TSZ_SMALL, town.is_city, _settings_game.economy.town_layout, false, 0, town.name);
|
||||
|
||||
TownID id = std::get<TownID>(result);
|
||||
|
||||
/* Check if the command failed. */
|
||||
if (id == INVALID_TOWN) return false;
|
||||
|
||||
/* The command succeeded, send the ID back through user_data. */
|
||||
town.town_id = id;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load town data from _file_to_saveload, place towns at the appropriate locations, and expand them to their target populations.
|
||||
*/
|
||||
void LoadTownData()
|
||||
{
|
||||
/* Load the JSON file as a string initially. We'll parse it soon. */
|
||||
size_t filesize;
|
||||
FILE *f = FioFOpenFile(_file_to_saveload.name, "rb", HEIGHTMAP_DIR, &filesize);
|
||||
|
||||
if (f == nullptr) {
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string text(filesize, '\0');
|
||||
size_t len = fread(text.data(), filesize, 1, f);
|
||||
FioFCloseFile(f);
|
||||
if (len != 1) {
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Now parse the JSON. */
|
||||
nlohmann::json town_data;
|
||||
try {
|
||||
town_data = nlohmann::json::parse(text);
|
||||
} catch (nlohmann::json::exception &) {
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check for JSON formatting errors with the array of towns. */
|
||||
if (!town_data.is_array()) {
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::pair<Town *, uint> > towns;
|
||||
uint failed_towns = 0;
|
||||
|
||||
/* Iterate through towns and attempt to found them. */
|
||||
for (auto &feature : town_data) {
|
||||
ExternalTownData town;
|
||||
|
||||
/* Ensure JSON is formatted properly. */
|
||||
if (!feature.is_object()) {
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check to ensure all fields exist and are of the correct type.
|
||||
* If the town name is formatted wrong, all we can do is give a general warning. */
|
||||
if (!feature.contains("name") || !feature.at("name").is_string()) {
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
/* If other fields are formatted wrong, we can actually inform the player which town is the problem. */
|
||||
if (!feature.contains("population") || !feature.at("population").is_number() ||
|
||||
!feature.contains("city") || !feature.at("city").is_boolean() ||
|
||||
!feature.contains("x") || !feature.at("x").is_number() ||
|
||||
!feature.contains("y") || !feature.at("y").is_number()) {
|
||||
feature.at("name").get_to(town.name);
|
||||
SetDParamStr(0, town.name);
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_TOWN_FORMATTED_INCORRECTLY, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set town properties. */
|
||||
feature.at("name").get_to(town.name);
|
||||
feature.at("population").get_to(town.population);
|
||||
feature.at("city").get_to(town.is_city);
|
||||
|
||||
/* Set town coordinates. */
|
||||
feature.at("x").get_to(town.x_proportion);
|
||||
feature.at("y").get_to(town.y_proportion);
|
||||
|
||||
/* Check for improper coordinates and warn the player. */
|
||||
if (town.x_proportion <= 0.0f || town.y_proportion <= 0.0f || town.x_proportion >= 1.0f || town.y_proportion >= 1.0f) {
|
||||
SetDParamStr(0, town.name);
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_LOAD_FAILED, STR_TOWN_DATA_ERROR_BAD_COORDINATE, WL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Find the target tile for the town. */
|
||||
TileIndex tile;
|
||||
switch (_settings_game.game_creation.heightmap_rotation) {
|
||||
case HM_CLOCKWISE:
|
||||
/* Tile coordinates align with what we expect. */
|
||||
tile = TileXY(town.x_proportion * Map::MaxX(), town.y_proportion * Map::MaxY());
|
||||
break;
|
||||
case HM_COUNTER_CLOCKWISE:
|
||||
/* Tile coordinates are rotated and must be adjusted. */
|
||||
tile = TileXY((1 - town.y_proportion * Map::MaxX()), town.x_proportion * Map::MaxY());
|
||||
break;
|
||||
default: NOT_REACHED();
|
||||
}
|
||||
|
||||
/* Try founding on the target tile, and if that doesn't work, find the nearest suitable tile up to 16 tiles away.
|
||||
* The target might be on water, blocked somehow, or on a steep slope that can't be terraformed by the founding command. */
|
||||
TileIndex search_tile = tile;
|
||||
bool success = CircularTileSearch(&search_tile, 16, 0, 0, TryFoundTownNearby, &town);
|
||||
|
||||
/* If we still fail to found the town, we'll create a sign at the intended location and tell the player how many towns we failed to create in an error message.
|
||||
* This allows the player to diagnose a heightmap misalignment, if towns end up in the sea, or place towns manually, if in rough terrain. */
|
||||
if (!success) {
|
||||
Command<CMD_PLACE_SIGN>::Post(tile, town.name);
|
||||
failed_towns++;
|
||||
continue;
|
||||
}
|
||||
|
||||
towns.emplace_back(std::make_pair(Town::Get(town.town_id), town.population));
|
||||
}
|
||||
|
||||
/* If we couldn't found a town (or multiple), display a message to the player with the number of failed towns. */
|
||||
if (failed_towns > 0) {
|
||||
SetDParam(0, failed_towns);
|
||||
ShowErrorMessage(STR_TOWN_DATA_ERROR_FAILED_TO_FOUND_TOWN, INVALID_STRING_ID, WL_WARNING);
|
||||
}
|
||||
|
||||
/* Now that we've created the towns, let's grow them to their target populations. */
|
||||
for (const auto &item : towns) {
|
||||
Town *t = item.first;
|
||||
uint population = item.second;
|
||||
|
||||
/* Grid towns can grow almost forever, but the town growth algorithm gets less and less efficient as it wanders roads randomly,
|
||||
* so we set an arbitrary limit. With a flat map and a 3x3 grid layout this results in about 4900 houses, or 2800 houses with "Better roads." */
|
||||
int try_limit = 1000;
|
||||
|
||||
/* If a town repeatedly fails to grow, continuing to try only wastes time. */
|
||||
int fail_limit = 10;
|
||||
|
||||
/* Grow by a constant number of houses each time, instead of growth based on current town size.
|
||||
* We want our try limit to apply in a predictable way, no matter the road layout and other geography. */
|
||||
const int HOUSES_TO_GROW = 10;
|
||||
|
||||
do {
|
||||
uint before = t->cache.num_houses;
|
||||
Command<CMD_EXPAND_TOWN>::Post(t->index, HOUSES_TO_GROW);
|
||||
if (t->cache.num_houses <= before) fail_limit--;
|
||||
} while (fail_limit > 0 && try_limit-- > 0 && t->cache.population < population);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ void GenerateWorld(GenWorldMode mode, uint size_x, uint size_y, bool reset_setti
|
|||
void AbortGeneratingWorld();
|
||||
bool IsGeneratingWorldAborted();
|
||||
void HandleGeneratingWorldAbortion();
|
||||
void LoadTownData();
|
||||
|
||||
/* genworld_gui.cpp */
|
||||
void SetNewLandscapeType(uint8_t landscape);
|
||||
|
|
|
@ -3019,6 +3019,8 @@ STR_FOUND_TOWN_RANDOM_TOWN_BUTTON :{BLACK}Random T
|
|||
STR_FOUND_TOWN_RANDOM_TOWN_TOOLTIP :{BLACK}Found town in random location
|
||||
STR_FOUND_TOWN_MANY_RANDOM_TOWNS :{BLACK}Many random towns
|
||||
STR_FOUND_TOWN_RANDOM_TOWNS_TOOLTIP :{BLACK}Cover the map with randomly placed towns
|
||||
STR_FOUND_TOWN_LOAD_FROM_FILE :{BLACK}Load from file
|
||||
STR_FOUND_TOWN_LOAD_FROM_FILE_TOOLTIP :{BLACK}Import town data from a JSON file
|
||||
STR_FOUND_TOWN_EXPAND_ALL_TOWNS :{BLACK}Expand all towns
|
||||
STR_FOUND_TOWN_EXPAND_ALL_TOWNS_TOOLTIP :{BLACK}Make all towns grow slightly
|
||||
|
||||
|
@ -3271,6 +3273,7 @@ STR_SAVELOAD_SAVE_SCENARIO :{WHITE}Save Sce
|
|||
STR_SAVELOAD_LOAD_SCENARIO :{WHITE}Load Scenario
|
||||
STR_SAVELOAD_LOAD_HEIGHTMAP :{WHITE}Load Heightmap
|
||||
STR_SAVELOAD_SAVE_HEIGHTMAP :{WHITE}Save Heightmap
|
||||
STR_SAVELOAD_LOAD_TOWN_DATA :{WHITE}Load Town Data
|
||||
STR_SAVELOAD_HOME_BUTTON :{BLACK}Click here to jump to the current default save/load directory
|
||||
STR_SAVELOAD_BYTES_FREE :{BLACK}{BYTES} free
|
||||
STR_SAVELOAD_LIST_TOOLTIP :{BLACK}List of drives, directories and saved-game files
|
||||
|
@ -3282,6 +3285,7 @@ STR_SAVELOAD_SAVE_TOOLTIP :{BLACK}Save the
|
|||
STR_SAVELOAD_LOAD_BUTTON :{BLACK}Load
|
||||
STR_SAVELOAD_LOAD_TOOLTIP :{BLACK}Load the selected game
|
||||
STR_SAVELOAD_LOAD_HEIGHTMAP_TOOLTIP :{BLACK}Load the selected heightmap
|
||||
STR_SAVELOAD_LOAD_TOWN_DATA_TOOLTIP :{BLACK}Load the selected town data
|
||||
STR_SAVELOAD_DETAIL_CAPTION :{BLACK}Game Details
|
||||
STR_SAVELOAD_DETAIL_NOT_AVAILABLE :{BLACK}No information available
|
||||
STR_SAVELOAD_DETAIL_COMPANY_INDEX :{SILVER}{COMMA}: {WHITE}{STRING1}
|
||||
|
@ -3415,6 +3419,13 @@ STR_GENERATION_PREPARING_TILELOOP :{BLACK}Running
|
|||
STR_GENERATION_PREPARING_SCRIPT :{BLACK}Running script
|
||||
STR_GENERATION_PREPARING_GAME :{BLACK}Preparing game
|
||||
|
||||
STR_TOWN_DATA_ERROR_LOAD_FAILED :{WHITE}Town data load failed
|
||||
STR_TOWN_DATA_ERROR_JSON_FORMATTED_INCORRECTLY :{WHITE}JSON file formatted incorrectly
|
||||
STR_TOWN_DATA_ERROR_TOWN_FORMATTED_INCORRECTLY :{WHITE}{RAW_STRING} data formatted incorrectly
|
||||
STR_TOWN_DATA_ERROR_BAD_COORDINATE :{WHITE}{RAW_STRING} coordinates formatted incorrectly, must be 0..1 as a percentage of the total heightmap dimension
|
||||
|
||||
STR_TOWN_DATA_ERROR_FAILED_TO_FOUND_TOWN :{WHITE}Could not find valid location to found {NUM} town{P "" s}. Created {P "a " ""}sign{P "" s} at the intended location{P "" s} instead
|
||||
|
||||
# NewGRF settings
|
||||
STR_NEWGRF_SETTINGS_CAPTION :{WHITE}NewGRF Settings
|
||||
STR_NEWGRF_SETTINGS_INFO_TITLE :{WHITE}Detailed NewGRF information
|
||||
|
|
|
@ -2168,15 +2168,13 @@ std::tuple<CommandCost, Money, TownID> CmdFoundTown(DoCommandFlag flags, TileInd
|
|||
Town *t;
|
||||
if (random_location) {
|
||||
t = CreateRandomTown(20, townnameparts, size, city, layout);
|
||||
if (t == nullptr) {
|
||||
cost = CommandCost(STR_ERROR_NO_SPACE_FOR_TOWN);
|
||||
} else {
|
||||
new_town = t->index;
|
||||
}
|
||||
if (t == nullptr) return { CommandCost(STR_ERROR_NO_SPACE_FOR_TOWN), 0, INVALID_TOWN };
|
||||
} else {
|
||||
t = new Town(tile);
|
||||
DoCreateTown(t, tile, townnameparts, size, city, layout, true);
|
||||
}
|
||||
|
||||
new_town = t->index;
|
||||
UpdateNearestTownForRoadTiles(false);
|
||||
old_generating_world.Restore();
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
#include "core/backup_type.hpp"
|
||||
#include "core/geometry_func.hpp"
|
||||
#include "genworld.h"
|
||||
#include "fios.h"
|
||||
#include "stringfilter_type.h"
|
||||
#include "dropdown_func.h"
|
||||
#include "town_kdtree.h"
|
||||
|
@ -1108,6 +1109,7 @@ static constexpr NWidgetPart _nested_found_town_widgets[] = {
|
|||
NWidget(WWT_TEXTBTN, COLOUR_GREY, WID_TF_NEW_TOWN), SetDataTip(STR_FOUND_TOWN_NEW_TOWN_BUTTON, STR_FOUND_TOWN_NEW_TOWN_TOOLTIP), SetFill(1, 0),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_RANDOM_TOWN), SetDataTip(STR_FOUND_TOWN_RANDOM_TOWN_BUTTON, STR_FOUND_TOWN_RANDOM_TOWN_TOOLTIP), SetFill(1, 0),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_MANY_RANDOM_TOWNS), SetDataTip(STR_FOUND_TOWN_MANY_RANDOM_TOWNS, STR_FOUND_TOWN_RANDOM_TOWNS_TOOLTIP), SetFill(1, 0),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_LOAD_FROM_FILE), SetDataTip(STR_FOUND_TOWN_LOAD_FROM_FILE, STR_FOUND_TOWN_LOAD_FROM_FILE_TOOLTIP), SetFill(1, 0),
|
||||
NWidget(WWT_PUSHTXTBTN, COLOUR_GREY, WID_TF_EXPAND_ALL_TOWNS), SetDataTip(STR_FOUND_TOWN_EXPAND_ALL_TOWNS, STR_FOUND_TOWN_EXPAND_ALL_TOWNS_TOOLTIP), SetFill(1, 0),
|
||||
|
||||
/* Town name selection. */
|
||||
|
@ -1188,7 +1190,7 @@ public:
|
|||
void UpdateButtons(bool check_availability)
|
||||
{
|
||||
if (check_availability && _game_mode != GM_EDITOR) {
|
||||
this->SetWidgetsDisabledState(true, WID_TF_RANDOM_TOWN, WID_TF_MANY_RANDOM_TOWNS, WID_TF_EXPAND_ALL_TOWNS, WID_TF_SIZE_LARGE);
|
||||
this->SetWidgetsDisabledState(true, WID_TF_RANDOM_TOWN, WID_TF_MANY_RANDOM_TOWNS, WID_TF_LOAD_FROM_FILE, WID_TF_EXPAND_ALL_TOWNS, WID_TF_SIZE_LARGE);
|
||||
this->SetWidgetsDisabledState(_settings_game.economy.found_town != TF_CUSTOM_LAYOUT,
|
||||
WID_TF_LAYOUT_ORIGINAL, WID_TF_LAYOUT_BETTER, WID_TF_LAYOUT_GRID2, WID_TF_LAYOUT_GRID3, WID_TF_LAYOUT_RANDOM);
|
||||
if (_settings_game.economy.found_town != TF_CUSTOM_LAYOUT) town_layout = _settings_game.economy.town_layout;
|
||||
|
@ -1254,6 +1256,10 @@ public:
|
|||
break;
|
||||
}
|
||||
|
||||
case WID_TF_LOAD_FROM_FILE:
|
||||
ShowSaveLoadDialog(FT_TOWN_DATA, SLO_LOAD);
|
||||
break;
|
||||
|
||||
case WID_TF_EXPAND_ALL_TOWNS:
|
||||
for (Town *t : Town::Iterate()) {
|
||||
Command<CMD_EXPAND_TOWN>::Do(DC_EXEC, t->index, 0);
|
||||
|
@ -1274,6 +1280,11 @@ public:
|
|||
case WID_TF_LAYOUT_ORIGINAL: case WID_TF_LAYOUT_BETTER: case WID_TF_LAYOUT_GRID2:
|
||||
case WID_TF_LAYOUT_GRID3: case WID_TF_LAYOUT_RANDOM:
|
||||
this->town_layout = (TownLayout)(widget - WID_TF_LAYOUT_ORIGINAL);
|
||||
|
||||
/* If we are in the editor, sync the settings of the current game to the chosen layout,
|
||||
* so that importing towns from file uses the selected layout. */
|
||||
if (_game_mode == GM_EDITOR) _settings_game.economy.town_layout = this->town_layout;
|
||||
|
||||
this->UpdateButtons(false);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ enum TownFoundingWidgets : WidgetID {
|
|||
WID_TF_NEW_TOWN, ///< Create a new town.
|
||||
WID_TF_RANDOM_TOWN, ///< Randomly place a town.
|
||||
WID_TF_MANY_RANDOM_TOWNS, ///< Randomly place many towns.
|
||||
WID_TF_LOAD_FROM_FILE, ///< Load town data from file.
|
||||
WID_TF_EXPAND_ALL_TOWNS, ///< Make all towns grow slightly.
|
||||
WID_TF_TOWN_NAME_EDITBOX, ///< Editor for the town name.
|
||||
WID_TF_TOWN_NAME_RANDOM, ///< Generate a random town name.
|
||||
|
|
Loading…
Reference in New Issue