summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--projects/openttd_vs80.vcproj20
-rw-r--r--projects/openttd_vs90.vcproj20
-rw-r--r--source.list5
-rw-r--r--src/ai/ai.hpp4
-rw-r--r--src/ai/ai_scanner.cpp138
-rw-r--r--src/ai/ai_scanner.hpp3
-rw-r--r--src/core/smallvec_type.hpp11
-rw-r--r--src/fileio.cpp38
-rw-r--r--src/fileio_func.h1
-rw-r--r--src/fileio_type.h1
-rw-r--r--src/gfx_func.h2
-rw-r--r--src/gfxinit.cpp33
-rw-r--r--src/intro_gui.cpp17
-rw-r--r--src/lang/english.txt57
-rw-r--r--src/network/core/config.h3
-rw-r--r--src/network/core/tcp_content.cpp136
-rw-r--r--src/network/core/tcp_content.h208
-rw-r--r--src/network/network.cpp3
-rw-r--r--src/network/network_content.cpp445
-rw-r--r--src/network/network_content.h79
-rw-r--r--src/network/network_content_gui.cpp841
-rw-r--r--src/network/network_gui.cpp36
-rw-r--r--src/newgrf_gui.cpp69
23 files changed, 2150 insertions, 20 deletions
diff --git a/projects/openttd_vs80.vcproj b/projects/openttd_vs80.vcproj
index e4ac63029..bba8af6d8 100644
--- a/projects/openttd_vs80.vcproj
+++ b/projects/openttd_vs80.vcproj
@@ -632,6 +632,10 @@
>
</File>
<File
+ RelativePath=".\..\src\network\network_content.cpp"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\network_gamelist.cpp"
>
</File>
@@ -1124,6 +1128,10 @@
>
</File>
<File
+ RelativePath=".\..\src\network\network_content.h"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\network_func.h"
>
</File>
@@ -1780,6 +1788,10 @@
>
</File>
<File
+ RelativePath=".\..\src\network\network_content_gui.cpp"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\network_gui.cpp"
>
</File>
@@ -3232,6 +3244,14 @@
>
</File>
<File
+ RelativePath=".\..\src\network\core\tcp_content.cpp"
+ >
+ </File>
+ <File
+ RelativePath=".\..\src\network\core\tcp_content.h"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\core\tcp_game.cpp"
>
</File>
diff --git a/projects/openttd_vs90.vcproj b/projects/openttd_vs90.vcproj
index 4a8176f67..1d861a933 100644
--- a/projects/openttd_vs90.vcproj
+++ b/projects/openttd_vs90.vcproj
@@ -629,6 +629,10 @@
>
</File>
<File
+ RelativePath=".\..\src\network\network_content.cpp"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\network_gamelist.cpp"
>
</File>
@@ -1121,6 +1125,10 @@
>
</File>
<File
+ RelativePath=".\..\src\network\network_content.h"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\network_func.h"
>
</File>
@@ -1777,6 +1785,10 @@
>
</File>
<File
+ RelativePath=".\..\src\network\network_content_gui.cpp"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\network_gui.cpp"
>
</File>
@@ -3229,6 +3241,14 @@
>
</File>
<File
+ RelativePath=".\..\src\network\core\tcp_content.cpp"
+ >
+ </File>
+ <File
+ RelativePath=".\..\src\network\core\tcp_content.h"
+ >
+ </File>
+ <File
RelativePath=".\..\src\network\core\tcp_game.cpp"
>
</File>
diff --git a/source.list b/source.list
index 90d0f2872..2809b970b 100644
--- a/source.list
+++ b/source.list
@@ -45,6 +45,7 @@ namegen.cpp
network/network.cpp
network/network_client.cpp
network/network_command.cpp
+network/network_content.cpp
network/network_gamelist.cpp
network/network_server.cpp
network/network_udp.cpp
@@ -211,6 +212,7 @@ namegen_func.h
network/network.h
network/network_base.h
network/network_client.h
+network/network_content.h
network/network_func.h
network/network_gamelist.h
network/network_gui.h
@@ -391,6 +393,7 @@ main_gui.cpp
misc_gui.cpp
music_gui.cpp
network/network_chat_gui.cpp
+network/network_content_gui.cpp
network/network_gui.cpp
newgrf_gui.cpp
news_gui.cpp
@@ -776,6 +779,8 @@ network/core/packet.cpp
network/core/packet.h
network/core/tcp.cpp
network/core/tcp.h
+network/core/tcp_content.cpp
+network/core/tcp_content.h
network/core/tcp_game.cpp
network/core/tcp_game.h
network/core/udp.cpp
diff --git a/src/ai/ai.hpp b/src/ai/ai.hpp
index c1a700f75..96dc93317 100644
--- a/src/ai/ai.hpp
+++ b/src/ai/ai.hpp
@@ -111,7 +111,9 @@ public:
static AIInfo *FindInfo(const char *name, int version);
static bool ImportLibrary(const char *library, const char *class_name, int version, HSQUIRRELVM vm);
static void Rescan();
-
+#if defined(ENABLE_NETWORK)
+ static bool HasAI(const struct ContentInfo *ci, bool md5sum);
+#endif
private:
static uint frame_counter;
static class AIScanner *ai_scanner;
diff --git a/src/ai/ai_scanner.cpp b/src/ai/ai_scanner.cpp
index 11f4cdafd..be0dfa59d 100644
--- a/src/ai/ai_scanner.cpp
+++ b/src/ai/ai_scanner.cpp
@@ -416,3 +416,141 @@ char *AIScanner::GetAIConsoleList(char *p, const char *last)
return p;
}
+
+#if defined(ENABLE_NETWORK)
+#include "../network/network_content.h"
+#include "../md5.h"
+#include "../tar_type.h"
+
+/** Helper for creating a MD5sum of all files within of an AI. */
+struct AIFileChecksumCreator : FileScanner {
+ byte md5sum[16]; ///< The final md5sum
+
+ /**
+ * Initialise the md5sum to be all zeroes,
+ * so we can easily xor the data.
+ */
+ AIFileChecksumCreator()
+ {
+ memset(this->md5sum, 0, sizeof(this->md5sum));
+ }
+
+ /* Add the file and calculate the md5 sum. */
+ virtual bool AddFile(const char *filename, size_t basepath_length)
+ {
+ Md5 checksum;
+ uint8 buffer[1024];
+ size_t len, size;
+ byte tmp_md5sum[16];
+
+ /* Open the file ... */
+ FILE *f = FioFOpenFile(filename, "rb", DATA_DIR, &size);
+ if (f == NULL) return false;
+
+ /* ... calculate md5sum... */
+ while ((len = fread(buffer, 1, (size > sizeof(buffer)) ? sizeof(buffer) : size, f)) != 0 && size != 0) {
+ size -= len;
+ checksum.Append(buffer, len);
+ }
+ checksum.Finish(tmp_md5sum);
+
+ FioFCloseFile(f);
+
+ /* ... and xor it to the overall md5sum. */
+ for (uint i = 0; i < sizeof(md5sum); i++) this->md5sum[i] ^= tmp_md5sum[i];
+
+ return true;
+ }
+};
+
+/**
+ * Check whether the AI given in info is the same as in ci based
+ * on the shortname and md5 sum.
+ * @param ci the information to compare to
+ * @param md5sum whether to check the MD5 checksum
+ * @param info the AI to get the shortname and md5 sum from
+ * @return true iff they're the same
+ */
+static bool IsSameAI(const ContentInfo *ci, bool md5sum, AIFileInfo *info)
+{
+ uint32 id = 0;
+ const char *str = info->GetShortName();
+ for (int j = 0; j < 4 && *str != '\0'; j++, str++) id |= *str << (8 * j);
+
+ if (id != ci->unique_id) return false;
+ if (!md5sum) return true;
+
+ AIFileChecksumCreator checksum;
+ char path[MAX_PATH];
+ strecpy(path, info->GetMainScript(), lastof(path));
+ /* There'll always be at least 2 path separator characters in an AI's
+ * main script name as the search algorithm requires the main script to
+ * be in a subdirectory of the AI directory; so ai/<path>/main.nut. */
+ *strrchr(path, PATHSEPCHAR) = '\0';
+ *strrchr(path, PATHSEPCHAR) = '\0';
+ TarList::iterator iter = _tar_list.find(path);
+
+ if (iter != _tar_list.end()) {
+ /* The main script is in a tar file, so find all files that
+ * are in the same tar and add them to the MD5 checksumming. */
+ TarFileList::iterator tar;
+ FOR_ALL_TARS(tar) {
+ /* Not in the same tar. */
+ if (tar->second.tar_filename != iter->first) continue;
+
+ /* Check the extension. */
+ const char *ext = strrchr(tar->first.c_str(), '.');
+ if (ext == NULL || strcasecmp(ext, ".nut") != 0) continue;
+
+ /* Create the full path name, */
+ seprintf(path, lastof(path), "%s%c%s", tar->second.tar_filename, PATHSEPCHAR, tar->first.c_str());
+ checksum.AddFile(path, 0);
+ }
+ } else {
+ /* Add the path sep char back when searching a directory, so we are
+ * in the actual directory. */
+ path[strlen(path)] = PATHSEPCHAR;
+ checksum.Scan(".nut", path);
+ }
+
+ return memcmp(ci->md5sum, checksum.md5sum, sizeof(ci->md5sum)) == 0;
+}
+
+/**
+ * Check whether we have an AI (library) with the exact characteristics as ci.
+ * @param ci the characteristics to search on (shortname and md5sum)
+ * @param md5sum whether to check the MD5 checksum
+ * @return true iff we have an AI (library) matching.
+ */
+bool AIScanner::HasAI(const ContentInfo *ci, bool md5sum)
+{
+ switch (ci->type) {
+ case CONTENT_TYPE_AI:
+ for (AIInfoList::iterator it = this->info_list.begin(); it != this->info_list.end(); it++) {
+ if (IsSameAI(ci, md5sum, (*it).second)) return true;
+ }
+ return false;
+
+ case CONTENT_TYPE_AI_LIBRARY:
+ for (AILibraryList::iterator it = this->library_list.begin(); it != this->library_list.end(); it++) {
+ if (IsSameAI(ci, md5sum, (*it).second)) return true;
+ }
+ return false;
+
+ default:
+ NOT_REACHED();
+ }
+}
+
+/**
+ * Check whether we have an AI (library) with the exact characteristics as ci.
+ * @param ci the characteristics to search on (shortname and md5sum)
+ * @param md5sum whether to check the MD5 checksum
+ * @return true iff we have an AI (library) matching.
+ */
+/*static */ bool AI::HasAI(const ContentInfo *ci, bool md5sum)
+{
+ return AI::ai_scanner->HasAI(ci, md5sum);
+}
+
+#endif /* ENABLE_NETWORK */
diff --git a/src/ai/ai_scanner.hpp b/src/ai/ai_scanner.hpp
index 3e82e4682..e79d8ec1d 100644
--- a/src/ai/ai_scanner.hpp
+++ b/src/ai/ai_scanner.hpp
@@ -64,6 +64,9 @@ public:
*/
void RescanAIDir();
+#if defined(ENABLE_NETWORK)
+ bool HasAI(const struct ContentInfo *ci, bool md5sum);
+#endif
private:
typedef std::map<const char *, class AILibrary *, ltstr> AILibraryList;
diff --git a/src/core/smallvec_type.hpp b/src/core/smallvec_type.hpp
index a7a0fbad4..f0ca48818 100644
--- a/src/core/smallvec_type.hpp
+++ b/src/core/smallvec_type.hpp
@@ -119,6 +119,17 @@ public:
return this->Find(item) != this->End();
}
+ /** Removes given item from this map
+ * @param item item to remove
+ * @return true iff key was found
+ * @note it has to be pointer to item in this map. It is overwritten by the last item.
+ */
+ FORCEINLINE void Erase(T *item)
+ {
+ assert(item >= this->Begin() && item < this->End());
+ *item = this->data[--this->items];
+ }
+
/**
* Tests whether a item is present in the vector, and appends it to the end if not.
* The '!=' operator of T is used for comparison.
diff --git a/src/fileio.cpp b/src/fileio.cpp
index 744da2e21..be6144160 100644
--- a/src/fileio.cpp
+++ b/src/fileio.cpp
@@ -518,7 +518,7 @@ static void SimplifyFileName(char *name)
#endif
}
-static bool TarListAddFile(const char *filename)
+bool TarListAddFile(const char *filename)
{
/* The TAR-header, repeated for every file */
typedef struct TarHeader {
@@ -951,6 +951,27 @@ void DeterminePaths(const char *exe)
free(save_dir);
free(autosave_dir);
+ /* If we have network we make a directory for the autodownloading of content */
+ _searchpaths[SP_AUTODOWNLOAD_DIR] = str_fmt("%s%s", _personal_dir, "content_download" PATHSEP);
+#ifdef ENABLE_NETWORK
+ FioCreateDirectory(_searchpaths[SP_AUTODOWNLOAD_DIR]);
+
+ /* Create the directory for each of the types of content */
+ const Subdirectory dirs[] = { SCENARIO_DIR, HEIGHTMAP_DIR, DATA_DIR, AI_DIR, AI_LIBRARY_DIR };
+ for (uint i = 0; i < lengthof(dirs); i++) {
+ char *tmp = str_fmt("%s%s", _searchpaths[SP_AUTODOWNLOAD_DIR], FioGetSubdirectory(dirs[i]));
+ FioCreateDirectory(tmp);
+ free(tmp);
+ }
+#else /* ENABLE_NETWORK */
+ /* If we don't have networking, we don't need to make the directory. But
+ * if it exists we keep it, otherwise remove it from the search paths. */
+ if (!FileExists(_searchpaths[SP_AUTODOWNLOAD_DIR])) {
+ free((void*)_searchpaths[SP_AUTODOWNLOAD_DIR]);
+ _searchpaths[SP_AUTODOWNLOAD_DIR] = NULL;
+ }
+#endif /* ENABLE_NETWORK */
+
ScanForTarFiles();
}
@@ -1097,3 +1118,18 @@ uint FileScanner::Scan(const char *extension, Subdirectory sd, bool tars)
return num;
}
+
+/**
+ * Scan for files with the given extention in the given search path.
+ * @param extension the extension of files to search for.
+ * @param directory the sub directory to search in.
+ * @return the number of found files, i.e. the number of times that
+ * AddFile returned true.
+ */
+uint FileScanner::Scan(const char *extension, const char *directory)
+{
+ char path[MAX_PATH];
+ strecpy(path, directory, lastof(path));
+ AppendPathSeparator(path, lengthof(path));
+ return ScanPath(this, extension, path, strlen(path));
+}
diff --git a/src/fileio_func.h b/src/fileio_func.h
index 352f7f7f2..d86696358 100644
--- a/src/fileio_func.h
+++ b/src/fileio_func.h
@@ -74,6 +74,7 @@ public:
virtual ~FileScanner() {}
uint Scan(const char *extension, Subdirectory sd, bool tars = true);
+ uint Scan(const char *extension, const char *directory);
/**
* Add a file with the given filename.
diff --git a/src/fileio_type.h b/src/fileio_type.h
index 0d992d1f5..fec162cce 100644
--- a/src/fileio_type.h
+++ b/src/fileio_type.h
@@ -36,6 +36,7 @@ enum Searchpath {
SP_BINARY_DIR, ///< Search in the directory where the binary resides
SP_INSTALLATION_DIR, ///< Search in the installation directory
SP_APPLICATION_BUNDLE_DIR, ///< Search within the application bundle
+ SP_AUTODOWNLOAD_DIR, ///< Search within the autodownload directory
NUM_SEARCHPATHS
};
diff --git a/src/gfx_func.h b/src/gfx_func.h
index 5a9077afa..b6a7a91ad 100644
--- a/src/gfx_func.h
+++ b/src/gfx_func.h
@@ -77,7 +77,7 @@ void UndrawMouseCursor();
enum {
/* Size of the buffer used for drawing strings. */
- DRAW_STRING_BUFFER = 1024,
+ DRAW_STRING_BUFFER = 2048,
};
void RedrawScreenRect(int left, int top, int right, int bottom);
diff --git a/src/gfxinit.cpp b/src/gfxinit.cpp
index 905bfe2a2..c23f0a816 100644
--- a/src/gfxinit.cpp
+++ b/src/gfxinit.cpp
@@ -543,3 +543,36 @@ char *GetGraphicsSetsList(char *p, const char *last)
return p;
}
+
+#if defined(ENABLE_NETWORK)
+#include "network/network_content.h"
+
+/**
+ * Check whether we have an graphics with the exact characteristics as ci.
+ * @param ci the characteristics to search on (shortname and md5sum)
+ * @param md5sum whether to check the MD5 checksum
+ * @return true iff we have an graphics set matching.
+ */
+bool HasGraphicsSet(const ContentInfo *ci, bool md5sum)
+{
+ assert(ci->type == CONTENT_TYPE_BASE_GRAPHICS);
+ for (const GraphicsSet *g = _available_graphics_sets; g != NULL; g = g->next) {
+ if (g->found_grfs <= 1) continue;
+
+ if (g->shortname != ci->unique_id) continue;
+ if (!md5sum) return true;
+
+ byte md5[16];
+ memset(md5, 0, sizeof(md5));
+ for (uint i = 0; i < MAX_GFT; i++) {
+ for (uint j = 0; j < sizeof(md5); j++) {
+ md5[j] ^= g->files[i].hash[j];
+ }
+ }
+ if (memcmp(md5, ci->md5sum, sizeof(md5)) == 0) return true;
+ }
+
+ return false;
+}
+
+#endif /* ENABLE_NETWORK */
diff --git a/src/intro_gui.cpp b/src/intro_gui.cpp
index 6cd94aa00..a2f5136ea 100644
--- a/src/intro_gui.cpp
+++ b/src/intro_gui.cpp
@@ -12,6 +12,7 @@
#include "heightmap.h"
#include "genworld.h"
#include "network/network_gui.h"
+#include "network/network_content.h"
#include "newgrf.h"
#include "strings_func.h"
#include "window_func.h"
@@ -24,8 +25,8 @@
#include "table/sprites.h"
static const Widget _select_game_widgets[] = {
-{ WWT_CAPTION, RESIZE_NONE, COLOUR_BROWN, 0, 335, 0, 13, STR_0307_OPENTTD, STR_NULL},
-{ WWT_PANEL, RESIZE_NONE, COLOUR_BROWN, 0, 335, 14, 194, 0x0, STR_NULL},
+{ WWT_CAPTION, RESIZE_NONE, COLOUR_BROWN, 0, 335, 0, 13, STR_0307_OPENTTD, STR_NULL},
+{ WWT_PANEL, RESIZE_NONE, COLOUR_BROWN, 0, 335, 14, 194, 0x0, STR_NULL},
{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 10, 167, 22, 33, STR_0140_NEW_GAME, STR_02FB_START_A_NEW_GAME},
{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 168, 325, 22, 33, STR_0141_LOAD_GAME, STR_02FC_LOAD_A_SAVED_GAME},
{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 10, 167, 40, 51, STR_029A_PLAY_SCENARIO, STR_0303_START_A_NEW_GAME_USING},
@@ -43,7 +44,8 @@ static const Widget _select_game_widgets[] = {
{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 10, 167, 157, 168, STR_CONFIG_PATCHES, STR_CONFIG_PATCHES_TIP},
{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 168, 325, 157, 168, STR_NEWGRF_SETTINGS_BUTTON, STR_NULL},
-{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 104, 231, 175, 186, STR_0304_QUIT, STR_0305_QUIT_OPENTTD},
+{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 10, 167, 175, 186, STR_CONTENT_INTRO_BUTTON, STR_CONTENT_INTRO_BUTTON_TIP},
+{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_ORANGE, 168, 325, 175, 186, STR_0304_QUIT, STR_0305_QUIT_OPENTTD},
{ WIDGETS_END},
};
@@ -70,6 +72,7 @@ private:
SGI_DIFFICULTIES,
SGI_PATCHES_OPTIONS,
SGI_GRF_SETTINGS,
+ SGI_CONTENT_DOWNLOAD,
SGI_EXIT,
};
@@ -123,6 +126,14 @@ public:
case SGI_DIFFICULTIES: ShowGameDifficulty(); break;
case SGI_PATCHES_OPTIONS: ShowPatchesSelection(); break;
case SGI_GRF_SETTINGS: ShowNewGRFSettings(true, true, false, &_grfconfig_newgame); break;
+ case SGI_CONTENT_DOWNLOAD:
+ if (!_network_available) {
+ ShowErrorMessage(INVALID_STRING_ID, STR_NETWORK_ERR_NOTAVAILABLE, 0, 0);
+ } else {
+ ShowNetworkContentListWindow();
+ }
+ break;
+
case SGI_EXIT: HandleExitGameRequest(); break;
}
}
diff --git a/src/lang/english.txt b/src/lang/english.txt
index 7f8a19e4c..e85163834 100644
--- a/src/lang/english.txt
+++ b/src/lang/english.txt
@@ -3687,3 +3687,60 @@ STR_CONFIG_PATCHES_NOISE_LEVEL :{LTBLUE}Allow t
STR_NOISE_IN_TOWN :{BLACK}Noise limit in town: {ORANGE}{COMMA}{BLACK} max: {ORANGE}{COMMA}
STR_STATION_NOISE :{BLACK}Noise generated: {GOLD}{COMMA}
########
+
+############ Downloading of content from the central server
+STR_CONTENT_NO_ZLIB :{WHITE}OpenTTD is build without "zlib" support...
+STR_CONTENT_NO_ZLIB_SUB :{WHITE}... downloading content is not possible!
+STR_CONTENT_TYPE_BASE_GRAPHICS :Base graphics
+STR_CONTENT_TYPE_NEWGRF :NewGRF
+STR_CONTENT_TYPE_AI :AI
+STR_CONTENT_TYPE_AI_LIBRARY :AI library
+STR_CONTENT_TYPE_SCENARIO :Scenario
+STR_CONTENT_TYPE_HEIGHTMAP :Heightmap
+STR_CONTENT_TITLE :{WHITE}Content downloading
+STR_CONTENT_TYPE_CAPTION :{BLACK}Type
+STR_CONTENT_TYPE_CAPTION_TIP :{BLACK}Type of the content
+STR_CONTENT_NAME_CAPTION :{BLACK}Name
+STR_CONTENT_NAME_CAPTION_TIP :{BLACK}Name of the content
+STR_CONTENT_MATRIX_TIP :{BLACK}Click on a line to see the details{}Click on the checkbox to select it for downloading
+STR_CONTENT_SELECT_ALL_CAPTION :{BLACK}Select all
+STR_CONTENT_SELECT_ALL_CAPTION_TIP :{BLACK}Mark all content to be downloaded
+STR_CONTENT_SELECT_UPDATES_CAPTION :{BLACK}Select updates
+STR_CONTENT_SELECT_UPDATES_CAPTION_TIP :{BLACK}Mark all content that is an update for existing content to be downloaded
+STR_CONTENT_UNSELECT_ALL_CAPTION :{BLACK}Unselect all
+STR_CONTENT_UNSELECT_ALL_CAPTION_TIP :{BLACK}Mark all content to be not downloaded
+STR_CONTENT_DOWNLOAD_CAPTION :{BLACK}Download
+STR_CONTENT_DOWNLOAD_CAPTION_TIP :{BLACK}Start downloading the selected content
+STR_CONTENT_TOTAL_DOWNLOAD_SIZE :{SILVER}Total download size: {WHITE}{BYTES}
+STR_CONTENT_DETAIL_TITLE :{SILVER}CONTENT INFO
+STR_CONTENT_DETAIL_SUBTITLE_UNSELECTED :{SILVER}You have not selected this to be downloaded
+STR_CONTENT_DETAIL_SUBTITLE_SELECTED :{SILVER}You have selected this to be downloaded
+STR_CONTENT_DETAIL_SUBTITLE_AUTOSELECTED :{SILVER}This dependency has been selected to be downloaded
+STR_CONTENT_DETAIL_SUBTITLE_ALREADY_HERE :{SILVER}You already have this
+STR_CONTENT_DETAIL_SUBTITLE_DOES_NOT_EXIST :{SILVER}This content is unknown and cannot be downloaded in OpenTTD
+STR_CONTENT_DETAIL_UPDATE :{SILVER}This is a replacement for an exising {STRING}
+STR_CONTENT_DETAIL_NAME :{SILVER}Name: {WHITE}{RAW_STRING}
+STR_CONTENT_DETAIL_VERSION :{SILVER}Version: {WHITE}{RAW_STRING}
+STR_CONTENT_DETAIL_DESCRIPTION :{SILVER}Description: {WHITE}{RAW_STRING}
+STR_CONTENT_DETAIL_URL :{SILVER}URL: {WHITE}{RAW_STRING}
+STR_CONTENT_DETAIL_TYPE :{SILVER}Type: {WHITE}{STRING}
+STR_CONTENT_DETAIL_FILESIZE :{SILVER}Download size: {WHITE}{BYTES}
+STR_CONTENT_DETAIL_SELECTED_BECAUSE_OF :{SILVER}Selected because of: {WHITE}{RAW_STRING}
+STR_CONTENT_DETAIL_DEPENDENCIES :{SILVER}Dependencies: {WHITE}{RAW_STRING}
+STR_CONTENT_DETAIL_TAGS :{SILVER}Tags: {WHITE}{RAW_STRING}
+
+STR_CONTENT_DOWNLOAD_TITLE :{WHITE}Downloading content...
+STR_CONTENT_DOWNLOAD_INITIALISE :{WHITE}Requesting files...
+STR_CONTENT_DOWNLOAD_FILE :{WHITE}Currently downloading {RAW_STRING} ({NUM} of {NUM})
+STR_CONTENT_DOWNLOAD_COMPLETE :{WHITE}Download complete
+STR_CONTENT_DOWNLOAD_PROGRESS_SIZE :{WHITE}{BYTES} of {BYTES} downloaded ({NUM} %)
+
+STR_CONTENT_ERROR_COULD_NOT_CONNECT :{WHITE}Could not connect to the content server...
+STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD :{WHITE}Downloading failed...
+STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD_CONNECTION_LOST :{WHITE}... connection lost
+STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD_FILE_NOT_WRITABLE :{WHITE}... file not writable
+STR_CONTENT_ERROR_COULD_NOT_EXTRACT :{WHITE}Could not decompress the downloaded file
+
+STR_CONTENT_INTRO_BUTTON :{BLACK}Check online content
+STR_CONTENT_INTRO_BUTTON_TIP :{BLACK}Check for new and updated content to download
+########
diff --git a/src/network/core/config.h b/src/network/core/config.h
index c41190cce..e2fbb5e3a 100644
--- a/src/network/core/config.h
+++ b/src/network/core/config.h
@@ -9,11 +9,14 @@
/** DNS hostname of the masterserver */
#define NETWORK_MASTER_SERVER_HOST "master.openttd.org"
+/** DNS hostname of the content server */
+#define NETWORK_CONTENT_SERVER_HOST "content.openttd.org"
/** Message sent to the masterserver to 'identify' this client as OpenTTD */
#define NETWORK_MASTER_SERVER_WELCOME_MESSAGE "OpenTTDRegister"
enum {
NETWORK_MASTER_SERVER_PORT = 3978, ///< The default port of the master server (UDP)
+ NETWORK_CONTENT_SERVER_PORT = 3978, ///< The default port of the content server (TCP)
NETWORK_DEFAULT_PORT = 3979, ///< The default port of the game server (TCP & UDP)
NETWORK_DEFAULT_DEBUGLOG_PORT = 3982, ///< The default port debug-log is sent too (TCP)
diff --git a/src/network/core/tcp_content.cpp b/src/network/core/tcp_content.cpp
new file mode 100644
index 000000000..32054a1e2
--- /dev/null
+++ b/src/network/core/tcp_content.cpp
@@ -0,0 +1,136 @@
+/* $Id$ */
+
+/**
+ * @file tcp_content.cpp Basic functions to receive and send Content packets.
+ */
+
+#ifdef ENABLE_NETWORK
+
+#include "../../stdafx.h"
+#include "../../debug.h"
+#include "tcp_content.h"
+
+ContentInfo::ContentInfo()
+{
+ memset(this, 0, sizeof(*this));
+}
+
+ContentInfo::~ContentInfo()
+{
+ free(this->dependencies);
+ free(this->tags);
+}
+
+size_t ContentInfo::Size() const
+{
+ size_t len = 0;
+ for (uint i = 0; i < this->tag_count; i++) len += strlen(this->tags[i]) + 1;
+
+ /* The size is never larger than the content info size plus the size of the
+ * tags and dependencies */
+ return sizeof(*this) +
+ sizeof(this->dependency_count) +
+ sizeof(*this->dependencies) * this->dependency_count;
+}
+
+bool ContentInfo::IsSelected() const
+{
+ switch (this->state) {
+ case ContentInfo::SELECTED:
+ case ContentInfo::AUTOSELECTED:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool ContentInfo::IsValid() const
+{
+ return this->state < ContentInfo::INVALID && this->type >= CONTENT_TYPE_BEGIN && this->type < CONTENT_TYPE_END;
+}
+
+NetworkRecvStatus NetworkContentSocketHandler::CloseConnection()
+{
+ this->has_quit = true;
+ return NETWORK_RECV_STATUS_OKAY;
+}
+
+void NetworkContentSocketHandler::Close()
+{
+ CloseConnection();
+ if (this->sock == INVALID_SOCKET) return;
+
+ closesocket(this->sock);
+ this->sock = INVALID_SOCKET;
+}
+
+/**
+ * Defines a simple (switch) case for each network packet
+ * @param type the packet type to create the case for
+ */
+#define CONTENT_COMMAND(type) case type: return this->NetworkPacketReceive_ ## type ## _command(p); break;
+
+/**
+ * Handle an incoming packets by sending it to the correct function.
+ * @param p the received packet
+ */
+bool NetworkContentSocketHandler::HandlePacket(Packet *p)
+{
+ PacketContentType type = (PacketContentType)p->Recv_uint8();
+
+ switch (this->HasClientQuit() ? PACKET_CONTENT_END : type) {
+ CONTENT_COMMAND(PACKET_CONTENT_CLIENT_INFO_LIST);
+ CONTENT_COMMAND(PACKET_CONTENT_CLIENT_INFO_ID);
+ CONTENT_COMMAND(PACKET_CONTENT_CLIENT_INFO_EXTID);
+ CONTENT_COMMAND(PACKET_CONTENT_CLIENT_INFO_EXTID_MD5);
+ CONTENT_COMMAND(PACKET_CONTENT_SERVER_INFO);
+ CONTENT_COMMAND(PACKET_CONTENT_CLIENT_CONTENT);
+ CONTENT_COMMAND(PACKET_CONTENT_SERVER_CONTENT);
+
+ default:
+ if (this->HasClientQuit()) {
+ DEBUG(net, 0, "[tcp/content] received invalid packet type %d from %s:%d", type, inet_ntoa(this->client_addr.sin_addr), ntohs(this->client_addr.sin_port));
+ } else {
+ DEBUG(net, 0, "[tcp/content] received illegal packet from %s:%d", inet_ntoa(this->client_addr.sin_addr), ntohs(this->client_addr.sin_port));
+ }
+ return false;
+ }
+}
+
+/**
+ * Receive a packet at UDP level
+ */
+void NetworkContentSocketHandler::Recv_Packets()
+{
+ Packet *p;
+ NetworkRecvStatus res;
+ while ((p = this->Recv_Packet(&res)) != NULL) {
+ bool cont = HandlePacket(p);
+ delete p;
+ if (!cont) return;
+ }
+}
+
+/**
+ * Create stub implementations for all receive commands that only
+ * show a warning that the given command is not available for the
+ * socket where the packet came from.
+ * @param type the packet type to create the stub for
+ */
+#define DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(type) \
+bool NetworkContentSocketHandler::NetworkPacketReceive_## type ##_command(Packet *p) { \
+ DEBUG(net, 0, "[tcp/content] received illegal packet type %d from %s:%d", \
+ type, inet_ntoa(this->client_addr.sin_addr), ntohs(this->client_addr.sin_port)); \
+ return false; \
+}
+
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_LIST);
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_ID);
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_EXTID);
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_EXTID_MD5);
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_SERVER_INFO);
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_CONTENT);
+DEFINE_UNAVAILABLE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_SERVER_CONTENT);
+
+#endif /* ENABLE_NETWORK */
diff --git a/src/network/core/tcp_content.h b/src/network/core/tcp_content.h
new file mode 100644
index 000000000..5b7c2b850
--- /dev/null
+++ b/src/network/core/tcp_content.h
@@ -0,0 +1,208 @@
+/* $Id$ */
+
+/**
+ * @file tcp_content.h Basic functions to receive and send TCP packets to/from the content server.
+ */
+
+#ifndef NETWORK_CORE_CONTENT_H
+#define NETWORK_CORE_CONTENT_H
+
+#ifdef ENABLE_NETWORK
+
+#include "os_abstraction.h"
+#include "tcp.h"
+#include "packet.h"
+#include "../../debug.h"
+
+/** The values in the enum are important; they are used as database 'keys' */
+enum ContentType {
+ CONTENT_TYPE_BEGIN = 1, ///< Helper to mark the begin of the types
+ CONTENT_TYPE_BASE_GRAPHICS = 1, ///< The content consists of base graphics
+ CONTENT_TYPE_NEWGRF = 2, ///< The content consists of a NewGRF
+ CONTENT_TYPE_AI = 3, ///< The content consists of an AI
+ CONTENT_TYPE_AI_LIBRARY = 4, ///< The content consists of an AI library
+ CONTENT_TYPE_SCENARIO = 5, ///< The content consists of a scenario
+ CONTENT_TYPE_HEIGHTMAP = 6, ///< The content consists of a heightmap
+ CONTENT_TYPE_END, ///< Helper to mark the end of the types
+};
+
+/** Enum with all types of TCP content packets. The order MUST not be changed **/
+enum PacketContentType {
+ PACKET_CONTENT_CLIENT_INFO_LIST, ///< Queries the content server for a list of info of a given content type
+ PACKET_CONTENT_CLIENT_INFO_ID, ///< Queries the content server for information about a list of internal IDs
+ PACKET_CONTENT_CLIENT_INFO_EXTID, ///< Queries the content server for information about a list of external IDs
+ PACKET_CONTENT_CLIENT_INFO_EXTID_MD5, ///< Queries the content server for information about a list of external IDs and MD5
+ PACKET_CONTENT_SERVER_INFO, ///< Reply of content server with information about content
+ PACKET_CONTENT_CLIENT_CONTENT, ///< Request a content file given an internal ID
+ PACKET_CONTENT_SERVER_CONTENT, ///< Reply with the content of the given ID
+ PACKET_CONTENT_END ///< Must ALWAYS be on the end of this list!! (period)
+};
+
+#define DECLARE_CONTENT_RECEIVE_COMMAND(type) virtual bool NetworkPacketReceive_## type ##_command(Packet *p)
+#define DEF_CONTENT_RECEIVE_COMMAND(cls, type) bool cls ##NetworkContentSocketHandler::NetworkPacketReceive_ ## type ## _command(Packet *p)
+
+enum ContentID {
+ INVALID_CONTENT_ID = UINT32_MAX
+};
+
+/** Container for all important information about a piece of content. */
+struct ContentInfo {
+ enum State {
+ UNSELECTED, ///< The content has not been selected
+ SELECTED, ///< The content has been manually selected
+ AUTOSELECTED, ///< The content has been selected as dependency
+ ALREADY_HERE, ///< The content is already at the client side
+ DOES_NOT_EXIST, ///< The content does not exist in the content system
+ INVALID ///< The content's invalid
+ };
+
+ ContentType type; ///< Type of content
+ ContentID id; ///< Unique (server side) ID for the content
+ uint32 filesize; ///< Size of the file
+ char filename[48]; ///< Filename (for the .tar.gz; only valid on download)
+ char name[32]; ///< Name of the content
+ char version[16]; ///< Version of the content
+ char url[64]; ///< URL related to the content
+ char description[512]; ///< Description of the content
+ uint32 unique_id; ///< Unique ID; either GRF ID or shortname
+ byte md5sum[16]; ///< The MD5 checksum
+ uint8 dependency_count; ///< Number of dependencies
+ ContentID *dependencies; ///< Malloced array of dependencies (unique server side ids)
+ uint8 tag_count; ///< Number of tags
+ char (*tags)[32]; ///< Malloced array of tags (strings)
+ State state; ///< Whether the content info is selected (for download)
+ bool update; ///< This item is an update
+
+ /** Clear everything in the struct */
+ ContentInfo();
+
+ /** Free everything allocated */
+ ~ContentInfo();
+
+ /**
+ * Get the size of the data as send over the network.
+ * @return the size.
+ */
+ size_t Size() const;
+
+ /**
+ * Is the state either selected or autoselected?
+ * @return true iff that's the case
+ */
+ bool IsSelected() const;
+
+ /**
+ * Is the information from this content info valid?
+ * @return true iff it's valid
+ */
+ bool IsValid() const;
+};
+
+/** Base socket handler for all Content TCP sockets */
+class NetworkContentSocketHandler : public NetworkTCPSocketHandler {
+protected:
+ struct sockaddr_in client_addr; ///< The address we're connected to.
+ NetworkRecvStatus CloseConnection();
+ void Close();
+
+ /**
+ * Client requesting a list of content info:
+ * byte type
+ * uint32 openttd version
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_LIST);
+
+ /**
+ * Client requesting a list of content info:
+ * uint16 count of ids
+ * uint32 id (count times)
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_ID);
+
+ /**
+ * Client requesting a list of content info based on an external
+ * 'unique' id; GRF ID for NewGRFS, shortname and for base
+ * graphics and AIs.
+ * Scenarios and AI libraries are not supported
+ * uint8 count of requests
+ * for each request:
+ * uint8 type
+ * unique id (uint32)
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_EXTID);
+
+ /**
+ * Client requesting a list of content info based on an external
+ * 'unique' id; GRF ID + MD5 checksum for NewGRFS, shortname and
+ * xor-ed MD5 checsums for base graphics and AIs.
+ * Scenarios and AI libraries are not supported
+ * uint8 count of requests
+ * for each request:
+ * uint8 type
+ * unique id (uint32)
+ * md5 (16 bytes)
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_INFO_EXTID_MD5);
+
+ /**
+ * Server sending list of content info:
+ * byte type (invalid ID == does not exist)
+ * uint32 id
+ * uint32 file_size
+ * string name (max 32 characters)
+ * string version (max 16 characters)
+ * uint32 unique id
+ * uint8 md5sum (16 bytes)
+ * uint8 dependency count
+ * uint32 unique id of dependency (dependency count times)
+ * uint8 tag count
+ * string tag (max 32 characters for tag count times)
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_SERVER_INFO);
+
+ /**
+ * Client requesting the actual content:
+ * uint16 count of unique ids
+ * uint32 unique id (count times)
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_CLIENT_CONTENT);
+
+ /**
+ * Server sending list of content info:
+ * uint32 unique id
+ * uint32 file size (0 == does not exist)
+ * string file name (max 48 characters)
+ * After this initial packet, packets with the actual data are send using
+ * the same packet type.
+ */
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_SERVER_CONTENT);
+
+ /**
+ * Handle the given packet, i.e. pass it to the right
+ * parser receive command.
+ * @param p the packet to handle
+ * @return true if we should immediatelly handle further packets, false otherwise
+ */
+ bool HandlePacket(Packet *p);
+public:
+ /**
+ * Create a new cs socket handler for a given cs
+ * @param s the socket we are connected with
+ * @param sin IP etc. of the client
+ */
+ NetworkContentSocketHandler(SOCKET s, const struct sockaddr_in *sin) :
+ NetworkTCPSocketHandler(s),
+ client_addr(*sin)
+ {
+ }
+
+ /** On destructing of this class, the socket needs to be closed */
+ virtual ~NetworkContentSocketHandler() { this->Close(); }
+
+ /** Do the actual receiving of packets. */
+ void Recv_Packets();
+};
+
+#endif /* ENABLE_NETWORK */
+
+#endif /* NETWORK_CORE_CONTENT_H */
diff --git a/src/network/network.cpp b/src/network/network.cpp
index 2379c46a2..79380cc87 100644
--- a/src/network/network.cpp
+++ b/src/network/network.cpp
@@ -18,6 +18,7 @@
#include "network_internal.h"
#include "network_client.h"
#include "network_server.h"
+#include "network_content.h"
#include "network_udp.h"
#include "network_gamelist.h"
#include "core/udp.h"
@@ -964,6 +965,8 @@ static bool NetworkDoClientLoop()
// We have to do some UDP checking
void NetworkUDPGameLoop()
{
+ NetworkContentLoop();
+
if (_network_udp_server) {
_udp_server_socket->ReceivePackets();
_udp_master_socket->ReceivePackets();
diff --git a/src/network/network_content.cpp b/src/network/network_content.cpp
new file mode 100644
index 000000000..d49e09282
--- /dev/null
+++ b/src/network/network_content.cpp
@@ -0,0 +1,445 @@
+/* $Id$ */
+
+/** @file network_content.cpp Content sending/receiving part of the network protocol. */
+
+#if defined(ENABLE_NETWORK)
+
+#include "../stdafx.h"
+#include "../debug.h"
+#include "../rev.h"
+#include "../core/alloc_func.hpp"
+#include "../core/math_func.hpp"
+#include "../newgrf_config.h"
+#include "../fileio_func.h"
+#include "../string_func.h"
+#include "../ai/ai.hpp"
+#include "../window_func.h"
+#include "../gui.h"
+#include "core/host.h"
+#include "network_content.h"
+
+#include "table/strings.h"
+
+#if defined(WITH_ZLIB)
+#include <zlib.h>
+#endif
+
+extern bool TarListAddFile(const char *filename);
+extern bool HasGraphicsSet(const ContentInfo *ci, bool md5sum);
+static ClientNetworkContentSocketHandler *_network_content_client;
+
+/** Wrapper function for the HasProc */
+static bool HasGRFConfig(const ContentInfo *ci, bool md5sum)
+{
+ return FindGRFConfig(BSWAP32(ci->unique_id), md5sum ? ci->md5sum : NULL) != NULL;
+}
+
+/**
+ * Check whether a function piece of content is locally known.
+ * Matches on the unique ID and possibly the MD5 checksum.
+ * @param ci the content info to search for
+ * @param md5sum also match the MD5 checksum?
+ * @return true iff it's known
+ */
+typedef bool (*HasProc)(const ContentInfo *ci, bool md5sum);
+
+DEF_CONTENT_RECEIVE_COMMAND(Client, PACKET_CONTENT_SERVER_INFO)
+{
+ ContentInfo *ci = new ContentInfo();
+ ci->type = (ContentType)p->Recv_uint8();
+ ci->id = (ContentID)p->Recv_uint32();
+ ci->filesize = p->Recv_uint32();
+
+ p->Recv_string(ci->name, lengthof(ci->name));
+ p->Recv_string(ci->version, lengthof(ci->name));
+ p->Recv_string(ci->url, lengthof(ci->url));
+ p->Recv_string(ci->description, lengthof(ci->description));
+
+ ci->unique_id = p->Recv_uint32();
+ for (uint j = 0; j < sizeof(ci->md5sum); j++) {
+ ci->md5sum[j] = p->Recv_uint8();
+ }
+
+ ci->dependency_count = p->Recv_uint8();
+ ci->dependencies = MallocT<ContentID>(ci->dependency_count);
+ for (uint i = 0; i < ci->dependency_count; i++) ci->dependencies[i] = (ContentID)p->Recv_uint32();
+
+ ci->tag_count = p->Recv_uint8();
+ ci->tags = MallocT<char[32]>(ci->tag_count);
+ for (uint i = 0; i < ci->tag_count; i++) p->Recv_string(ci->tags[i], lengthof(*ci->tags));
+
+ if (!ci->IsValid()) {
+ delete ci;
+ this->Close();
+ return false;
+ }
+
+ /* Find the appropriate check function */
+ HasProc proc = NULL;
+ switch (ci->type) {
+ case CONTENT_TYPE_NEWGRF:
+ proc = HasGRFConfig;
+ break;
+
+ case CONTENT_TYPE_BASE_GRAPHICS:
+ proc = HasGraphicsSet;
+ break;
+
+ case CONTENT_TYPE_AI:
+ case CONTENT_TYPE_AI_LIBRARY:
+ proc = AI::HasAI; break;
+ break;
+
+ default:
+ break;
+ }
+
+ if (proc != NULL) {
+ if (proc(ci, true)) {
+ ci->state = ContentInfo::ALREADY_HERE;
+ } else {
+ ci->state = ContentInfo::UNSELECTED;
+ if (proc(ci, false)) ci->update = true;
+ }
+ } else {
+ ci->state = ContentInfo::UNSELECTED;
+ }
+
+ /* Something we don't have and has filesize 0 does not exist in te system */
+ if (ci->state == ContentInfo::UNSELECTED && ci->filesize == 0) ci->state = ContentInfo::DOES_NOT_EXIST;
+
+ for (ContentCallback **iter = this->callbacks.Begin(); iter != this->callbacks.End(); iter++) {
+ (*iter)->OnReceiveContentInfo(ci);
+ }
+
+ return true;
+}
+
+void ClientNetworkContentSocketHandler::RequestContentList(ContentType type)
+{
+ Packet *p = new Packet(PACKET_CONTENT_CLIENT_INFO_LIST);
+ p->Send_uint8 ((byte)type);
+ p->Send_uint32(_openttd_newgrf_version);
+
+ this->Send_Packet(p);
+}
+
+void ClientNetworkContentSocketHandler::RequestContentList(uint count, const ContentID *content_ids)
+{
+ while (count > 0) {
+ /* We can "only" send a limited number of IDs in a single packet.
+ * A packet begins with the packet size and a byte for the type.
+ * Then this packet adds a byte for the content type and a uint16
+ * for the count in this packet. The rest of the packet can be
+ * used for the IDs. */
+ uint p_count = min(count, (SEND_MTU - sizeof(PacketSize) - sizeof(byte) - sizeof(byte) - sizeof(uint16)) / sizeof(uint32));
+
+ Packet *p = new Packet(PACKET_CONTENT_CLIENT_INFO_ID);
+ p->Send_uint16(p_count);
+
+ for (uint i = 0; i < p_count; i++) {
+ p->Send_uint32(content_ids[i]);
+ }
+
+ this->Send_Packet(p);
+ count -= p_count;
+ content_ids += count;
+ }
+}
+
+void ClientNetworkContentSocketHandler::RequestContentList(ContentVector *cv, bool send_md5sum)
+{
+ if (cv == NULL) return;
+
+ /* 20 is sizeof(uint32) + sizeof(md5sum (byte[16])) */
+ assert(cv->Length() < 255);
+ assert(cv->Length() < (SEND_MTU - sizeof(PacketSize) - sizeof(byte) - sizeof(uint8)) / (send_md5sum ? 20 : sizeof(uint32)));
+
+ Packet *p = new Packet(send_md5sum ? PACKET_CONTENT_CLIENT_INFO_EXTID_MD5 : PACKET_CONTENT_CLIENT_INFO_EXTID);
+ p->Send_uint8(cv->Length());
+
+ for (ContentIterator iter = cv->Begin(); iter != cv->End(); iter++) {
+ const ContentInfo *ci = *iter;
+ p->Send_uint8((byte)ci->type);
+ p->Send_uint32(ci->unique_id);
+ if (!send_md5sum) continue;
+
+ for (uint j = 0; j < sizeof(ci->md5sum); j++) {
+ p->Send_uint8(ci->md5sum[j]);
+ }
+ }
+
+ this->Send_Packet(p);
+}
+
+void ClientNetworkContentSocketHandler::RequestContent(uint count, const uint32 *content_ids)
+{
+ while (count > 0) {
+ /* We can "only" send a limited number of IDs in a single packet.
+ * A packet begins with the packet size and a byte for the type.
+ * Then this packet adds a uint16 for the count in this packet.
+ * The rest of the packet can be used for the IDs. */
+ uint p_count = min(count, (SEND_MTU - sizeof(PacketSize) - sizeof(byte) - sizeof(uint16)) / sizeof(uint32));
+
+ Packet *p = new Packet(PACKET_CONTENT_CLIENT_CONTENT);
+ p->Send_uint16(p_count);
+
+ for (uint i = 0; i < p_count; i++) {
+ p->Send_uint32(content_ids[i]);
+ }
+
+ this->Send_Packet(p);
+ count -= p_count;
+ content_ids += count;
+ }
+}
+
+/**
+ * Determine the full filename of a piece of content information
+ * @param ci the information to get the filename from
+ * @param compressed should the filename end with .gz?
+ * @return a statically allocated buffer with the filename or
+ * NULL when no filename could be made.
+ */
+static char *GetFullFilename(const ContentInfo *ci, bool compressed)
+{
+ Subdirectory dir;
+ switch (ci->type) {
+ default: return NULL;
+ case CONTENT_TYPE_BASE_GRAPHICS: dir = DATA_DIR; break;
+ case CONTENT_TYPE_NEWGRF: dir = DATA_DIR; break;
+ case CONTENT_TYPE_AI: dir = AI_DIR; break;
+ case CONTENT_TYPE_AI_LIBRARY: dir = AI_LIBRARY_DIR; break;
+ case CONTENT_TYPE_SCENARIO: dir = SCENARIO_DIR; break;
+ case CONTENT_TYPE_HEIGHTMAP: dir = HEIGHTMAP_DIR; break;
+ }
+
+ static char buf[MAX_PATH];
+ FioGetFullPath(buf, lengthof(buf), SP_AUTODOWNLOAD_DIR, dir, ci->filename);
+ strecat(buf, compressed ? ".tar.gz" : ".tar", lastof(buf));
+
+ return buf;
+}
+
+/**
+ * Gunzip a given file and remove the .gz if successful.
+ * @param ci container with filename
+ * @return true if the gunzip completed
+ */
+static bool GunzipFile(const ContentInfo *ci)
+{
+#if defined(WITH_ZLIB)
+ bool ret = true;
+ gzFile fin = gzopen(GetFullFilename(ci, true), "rb");
+ FILE *fout = fopen(GetFullFilename(ci, false), "wb");
+
+ if (fin == NULL || fout == NULL) {
+ ret = false;
+ goto exit;
+ }
+
+ byte buff[8192];
+ while (!gzeof(fin)) {
+ int read = gzread(fin, buff, sizeof(buff));
+ if (read < 0 || (size_t)read != fwrite(buff, 1, read, fout)) {
+ ret = false;
+ break;
+ }
+ }
+
+exit:
+ if (fin != NULL) gzclose(fin);
+ if (fout != NULL) fclose(fout);
+
+ return ret;
+#else
+ NOT_REACHED();
+#endif /* defined(WITH_ZLIB) */
+}
+
+DEF_CONTENT_RECEIVE_COMMAND(Client, PACKET_CONTENT_SERVER_CONTENT)
+{
+ if (this->curFile == NULL) {
+ /* When we haven't opened a file this must be our first packet with metadata. */
+ this->curInfo = new ContentInfo;
+ this->curInfo->type = (ContentType)p->Recv_uint8();
+ this->curInfo->id = (ContentID)p->Recv_uint32();
+ this->curInfo->filesize = p->Recv_uint32();
+ p->Recv_string(this->curInfo->filename, lengthof(this->curInfo->filename));
+
+ if (!this->curInfo->IsValid()) {
+ delete this->curInfo;
+ this->curInfo = NULL;
+ this->Close();
+ return false;
+ }
+
+ if (this->curInfo->filesize != 0) {
+ /* The filesize is > 0, so we are going to download it */
+ const char *filename = GetFullFilename(this->curInfo, true);
+ if (filename == NULL) {
+ /* Unless that fails ofcourse... */
+ DeleteWindowById(WC_NETWORK_STATUS_WINDOW, 0);
+ ShowErrorMessage(STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD, STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD_FILE_NOT_WRITABLE, 0, 0);
+ this->Close();
+ return false;
+ }
+
+ this->curFile = fopen(filename, "wb");
+ }
+ } else {
+ /* We have a file opened, thus are downloading internal content */
+ size_t toRead = (size_t)(p->size - p->pos);
+ if (fwrite(p->buffer + p->pos, 1, toRead, this->curFile) != toRead) {
+ DeleteWindowById(WC_NETWORK_STATUS_WINDOW, 0);
+ ShowErrorMessage(STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD, STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD_FILE_NOT_WRITABLE, 0, 0);
+ this->Close();
+ fclose(this->curFile);
+ this->curFile = NULL;
+
+ return false;
+ }
+
+ /* Tell the rest we downloaded some bytes */
+ for (ContentCallback **iter = this->callbacks.Begin(); iter != this->callbacks.End(); iter++) {
+ (*iter)->OnDownloadProgress(this->curInfo, toRead);
+ }
+
+ if (toRead == 0) {
+ /* We read nothing; that's our marker for end-of-stream.
+ * Now gunzip the tar and make it known. */
+ fclose(this->curFile);
+ this->curFile = NULL;
+
+ if (GunzipFile(this->curInfo)) {
+ unlink(GetFullFilename(this->curInfo, true));
+
+ TarListAddFile(GetFullFilename(this->curInfo, false));
+
+ for (ContentCallback **iter = this->callbacks.Begin(); iter != this->callbacks.End(); iter++) {
+ (*iter)->OnDownloadComplete(this->curInfo->id);
+ }
+ } else {
+ ShowErrorMessage(INVALID_STRING_ID, STR_CONTENT_ERROR_COULD_NOT_EXTRACT, 0, 0);
+ }
+ }
+ }
+
+ /* We ended this file, so clean up the mess */
+ if (this->curFile == NULL) {
+ delete this->curInfo;
+ this->curInfo = NULL;
+ }
+
+ return true;
+}
+
+/**
+ * Create a socket handler with the given socket and (server) address.
+ * @param s the socket to communicate over
+ * @param sin the IP/port of the server
+ */
+ClientNetworkContentSocketHandler::ClientNetworkContentSocketHandler(SOCKET s, const struct sockaddr_in *sin) :
+ NetworkContentSocketHandler(s, sin),
+ curFile(NULL),
+ curInfo(NULL)
+{
+}
+
+/** Clear up the mess ;) */
+ClientNetworkContentSocketHandler::~ClientNetworkContentSocketHandler()
+{
+ delete this->curInfo;
+ if (this->curFile != NULL) fclose(this->curFile);
+}
+
+/**
+ * Connect to the content server if needed, return the socket handler and
+ * add the callback to the socket handler.
+ * @param cb the callback to add
+ * @return the socket handler or NULL is connecting failed
+ */
+ClientNetworkContentSocketHandler *NetworkContent_Connect(ContentCallback *cb)
+{
+ /* If there's no connection or when it's broken, we try to reconnect.
+ * Otherwise we just add ourselves and return immediatelly */
+ if (_network_content_client != NULL && !_network_content_client->has_quit) {
+ *_network_content_client->callbacks.Append() = cb;
+ return _network_content_client;
+ }
+
+ SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
+ if (s == INVALID_SOCKET) return NULL;
+
+ if (!SetNoDelay(s)) DEBUG(net, 1, "Setting TCP_NODELAY failed");
+
+ struct sockaddr_in sin;
+ sin.sin_family = AF_INET;
+ sin.sin_addr.s_addr = NetworkResolveHost(NETWORK_CONTENT_SERVER_HOST);
+ sin.sin_port = htons(NETWORK_CONTENT_SERVER_PORT);
+
+ /* We failed to connect for which reason what so ever */
+ if (connect(s, (struct sockaddr*)&sin, sizeof(sin)) != 0) return NULL;
+
+ if (!SetNonBlocking(s)) DEBUG(net, 0, "Setting non-blocking mode failed"); // XXX should this be an error?
+
+ if (_network_content_client != NULL) {
+ if (_network_content_client->sock != INVALID_SOCKET) closesocket(_network_content_client->sock);
+ _network_content_client->sock = s;
+
+ /* Clean up the mess that could've been left behind */
+ _network_content_client->has_quit = false;
+ delete _network_content_client->curInfo;
+ _network_content_client->curInfo = NULL;
+ if (_network_content_client->curFile != NULL) fclose(_network_content_client->curFile);
+ _network_content_client->curFile = NULL;
+ } else {
+ _network_content_client = new ClientNetworkContentSocketHandler(s, &sin);
+ }
+ *_network_content_client->callbacks.Append() = cb;
+ return _network_content_client;
+}
+
+/**
+ * Remove yourself from the network content callback list and when
+ * that list is empty the connection will be closed.
+ */
+void NetworkContent_Disconnect(ContentCallback *cb)
+{
+ _network_content_client->callbacks.Erase(_network_content_client->callbacks.Find(cb));
+
+ if (_network_content_client->callbacks.Length() == 0) {
+ delete _network_content_client;
+ _network_content_client = NULL;
+ }
+}
+
+/**
+ * Check whether we received/can send some data from/to the content server and
+ * when that's the case handle it appropriately
+ */
+void NetworkContentLoop()
+{
+ if (_network_content_client == NULL) return;
+
+ fd_set read_fd, write_fd;
+ struct timeval tv;
+
+ FD_ZERO(&read_fd);
+ FD_ZERO(&write_fd);
+
+ FD_SET(_network_content_client->sock, &read_fd);
+ FD_SET(_network_content_client->sock, &write_fd);
+
+ tv.tv_sec = tv.tv_usec = 0; // don't block at all.
+#if !defined(__MORPHOS__) && !defined(__AMIGA__)
+ select(FD_SETSIZE, &read_fd, &write_fd, NULL, &tv);
+#else
+ WaitSelect(FD_SETSIZE, &read_fd, &write_fd, NULL, &tv, NULL);
+#endif
+ if (FD_ISSET(_network_content_client->sock, &read_fd)) _network_content_client->Recv_Packets();
+ _network_content_client->writable = !!FD_ISSET(_network_content_client->sock, &write_fd);
+ _network_content_client->Send_Packets();
+}
+
+#endif /* ENABLE_NETWORK */
diff --git a/src/network/network_content.h b/src/network/network_content.h
new file mode 100644
index 000000000..bcbb05f8f
--- /dev/null
+++ b/src/network/network_content.h
@@ -0,0 +1,79 @@
+/* $Id$ */
+
+/** @file network_content.h Part of the network protocol handling content distribution. */
+
+#ifndef NETWORK_CONTENT_H
+#define NETWORK_CONTENT_H
+
+#if defined(ENABLE_NETWORK)
+
+#include "core/tcp_content.h"
+#include "../core/smallvec_type.hpp"
+
+/** Vector with content info */
+typedef SmallVector<ContentInfo *, 16> ContentVector;
+/** Iterator for the content vector */
+typedef ContentInfo **ContentIterator;
+
+/** Callbacks for notifying others about incoming data */
+struct ContentCallback {
+ /**
+ * We received a content info.
+ * @param ci the content info
+ */
+ virtual void OnReceiveContentInfo(ContentInfo *ci) {}
+
+ /**
+ * We have progress in the download of a file
+ * @param ci the content info of the file
+ * @param bytes the number of bytes downloaded since the previous call
+ */
+ virtual void OnDownloadProgress(ContentInfo *ci, uint bytes) {}
+
+ /**
+ * We have finished downloading a file
+ * @param cid the ContentID of the downloaded file
+ */
+ virtual void OnDownloadComplete(ContentID cid) {}
+
+ /** Silentium */
+ virtual ~ContentCallback() {}
+};
+
+/**
+ * Socket handler for the content server connection
+ */
+class ClientNetworkContentSocketHandler : public NetworkContentSocketHandler {
+protected:
+ SmallVector<ContentCallback *, 2> callbacks; ///< Callbacks to notify "the world"
+
+ FILE *curFile; ///< Currently downloaded file
+ ContentInfo *curInfo; ///< Information about the currently downloaded file
+
+ friend ClientNetworkContentSocketHandler *NetworkContent_Connect(ContentCallback *cb);
+ friend void NetworkContent_Disconnect(ContentCallback *cb);
+
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_SERVER_INFO);
+ DECLARE_CONTENT_RECEIVE_COMMAND(PACKET_CONTENT_SERVER_CONTENT);
+
+ ClientNetworkContentSocketHandler(SOCKET s, const struct sockaddr_in *sin);
+ ~ClientNetworkContentSocketHandler();
+public:
+ void RequestContentList(ContentType type);
+ void RequestContentList(uint count, const ContentID *content_ids);
+ void RequestContentList(ContentVector *cv, bool send_md5sum = true);
+
+ void RequestContent(uint count, const uint32 *content_ids);
+};
+
+ClientNetworkContentSocketHandler *NetworkContent_Connect(ContentCallback *cb);
+void NetworkContent_Disconnect(ContentCallback *cb);
+void NetworkContentLoop();
+
+void ShowNetworkContentListWindow(ContentVector *cv = NULL, ContentType type = CONTENT_TYPE_END);
+
+#else
+static inline void ShowNetworkContentListWindow() {}
+#endif /* ENABLE_NETWORK */
+
+#endif /* NETWORK_CONTENT_H */
diff --git a/src/network/network_content_gui.cpp b/src/network/network_content_gui.cpp
new file mode 100644
index 000000000..2f9d481ea
--- /dev/null
+++ b/src/network/network_content_gui.cpp
@@ -0,0 +1,841 @@
+/* $Id$ */
+
+/** @file network_gui.cpp Implementation of the Network related GUIs. */
+
+#if defined(ENABLE_NETWORK)
+#include "../stdafx.h"
+#include "../string_func.h"
+#include "../strings_func.h"
+#include "../gfx_func.h"
+#include "../window_func.h"
+#include "../window_gui.h"
+#include "../gui.h"
+#include "../core/smallvec_type.hpp"
+#include "../ai/ai.hpp"
+#include "../gfxinit.h"
+#include "network_content.h"
+
+#include "table/strings.h"
+#include "../table/sprites.h"
+
+/** Widgets for the download window */
+static const Widget _network_content_download_status_window_widget[] = {
+{ WWT_CAPTION, RESIZE_NONE, COLOUR_GREY, 0, 349, 0, 13, STR_CONTENT_DOWNLOAD_TITLE, STR_018C_WINDOW_TITLE_DRAG_THIS}, // NCDSWW_CAPTION
+{ WWT_PANEL, RESIZE_NONE, COLOUR_GREY, 0, 349, 14, 84, 0x0, STR_NULL}, // NCDSWW_BACKGROUND
+{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_WHITE, 125, 225, 69, 80, STR_012E_CANCEL, STR_NULL}, // NCDSWW_CANCELOK
+{ WIDGETS_END},
+};
+
+/** Window description for the download window */
+static const WindowDesc _network_content_download_status_window_desc = {
+ WDP_CENTER, WDP_CENTER, 350, 85, 350, 85,
+ WC_NETWORK_STATUS_WINDOW, WC_NONE,
+ WDF_STD_TOOLTIPS | WDF_DEF_WIDGET | WDF_MODAL,
+ _network_content_download_status_window_widget,
+};
+
+/** Window for showing the download status of content */
+struct NetworkContentDownloadStatusWindow : public Window, ContentCallback {
+ /** Widgets used by this window */
+ enum Widgets {
+ NCDSWW_CAPTION, ///< Caption of the window
+ NCDSWW_BACKGROUND, ///< Background
+ NCDSWW_CANCELOK, ///< Cancel/OK button
+ };
+
+private:
+ ClientNetworkContentSocketHandler *connection; ///< Our connection with the content server
+ SmallVector<ContentType, 4> receivedTypes; ///< Types we received so we can update their cache
+
+ uint total_files; ///< Number of files to download
+ uint downloaded_files; ///< Number of files downloaded
+ uint total_bytes; ///< Number of bytes to download
+ uint downloaded_bytes; ///< Number of bytes downloaded
+
+ uint32 cur_id; ///< The current ID of the downloaded file
+ char name[32]; ///< The current name of the downloaded file
+
+public:
+ /**
+ * Create a new download window based on a list of content information
+ * with flags whether to download them or not.
+ * @param infos the list to search in
+ */
+ NetworkContentDownloadStatusWindow(ContentVector &infos) :
+ Window(&_network_content_download_status_window_desc),
+ cur_id(UINT32_MAX)
+ {
+ this->parent = FindWindowById(WC_NETWORK_WINDOW, 1);
+ this->connection = NetworkContent_Connect(this);
+
+ if (this->connection == NULL) {
+ /* When the connection got broken and we can't rebuild it we can't do much :( */
+ ShowErrorMessage(STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD, STR_CONTENT_ERROR_COULD_NOT_DOWNLOAD_CONNECTION_LOST, 0, 0);
+ delete this;
+ return;
+ }
+
+ /** Make the list of items to download */
+ uint32 *ids = MallocT<uint32>(infos.Length());
+ for (ContentIterator iter = infos.Begin(); iter != infos.End(); iter++) {
+ const ContentInfo *ci = *iter;
+ if (!ci->IsSelected()) continue;
+
+ ids[this->total_files++] = ci->id;
+ this->total_bytes += ci->filesize;
+ }
+
+ /** And request them for download */
+ this->connection->RequestContent(this->total_files, ids);
+ free(ids);
+
+ this->FindWindowPlacementAndResize(&_network_content_download_status_window_desc);
+ }
+
+ /** Free whatever we've allocated */
+ ~NetworkContentDownloadStatusWindow()
+ {
+ /* Tell all the backends about what we've downloaded */
+ for (ContentType *iter = this->receivedTypes.Begin(); iter != this->receivedTypes.End(); iter++) {
+ switch (*iter) {
+ case CONTENT_TYPE_AI:
+ case CONTENT_TYPE_AI_LIBRARY:
+ AI::Rescan();
+ InvalidateWindowClasses(WC_AI_DEBUG);
+ break;
+
+ case CONTENT_TYPE_BASE_GRAPHICS:
+ FindGraphicsSets();
+ break;
+
+ case CONTENT_TYPE_NEWGRF:
+ ScanNewGRFFiles();
+ /* Yes... these are the NewGRF windows */
+ InvalidateWindowClasses(WC_SAVELOAD);
+ InvalidateWindowData(WC_GAME_OPTIONS, 0, 1);
+ InvalidateWindowData(WC_NETWORK_WINDOW, 0, 2);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ NetworkContent_Disconnect(this);
+ }
+
+ virtual void OnPaint()
+ {
+ /* When downloading is finished change cancel in ok */
+ if (this->downloaded_bytes == this->total_bytes) {
+ this->widget[NCDSWW_CANCELOK].data = STR_012F_OK;
+ }
+
+ this->DrawWidgets();
+
+ /* Draw nice progress bar :) */
+ DrawFrameRect(20, 18, (int)((this->width - 20) * this->downloaded_bytes / this->total_bytes), 28, COLOUR_MAUVE, FR_NONE);
+
+ SetDParam(0, this->downloaded_bytes);
+ SetDParam(1, this->total_bytes);
+ SetDParam(2, this->downloaded_bytes * 100 / this->total_bytes);
+ DrawStringCentered(this->width / 2, 35, STR_CONTENT_DOWNLOAD_PROGRESS_SIZE, TC_GREY);
+
+ if (this->downloaded_bytes == this->total_bytes) {
+ DrawStringCentered(this->width / 2, 46, STR_CONTENT_DOWNLOAD_COMPLETE, TC_GREY);
+ } else if (!StrEmpty(this->name)) {
+ SetDParamStr(0, this->name);
+ SetDParam(1, this->downloaded_files);
+ SetDParam(2, this->total_files);
+ DrawStringCentered(this->width / 2, 46, STR_CONTENT_DOWNLOAD_FILE, TC_GREY);
+ } else {
+ DrawStringCentered(this->width / 2, 46, STR_CONTENT_DOWNLOAD_INITIALISE, TC_GREY);
+ }
+ }
+
+ virtual void OnClick(Point pt, int widget)
+ {
+ if (widget == NCDSWW_CANCELOK) delete this;
+ }
+
+ virtual void OnDownloadProgress(ContentInfo *ci, uint bytes)
+ {
+ if (ci->id != this->cur_id) {
+ strecpy(this->name, ci->filename, lastof(this->name));
+ this->cur_id = ci->id;
+ this->downloaded_files++;
+ this->receivedTypes.Include(ci->type);
+ }
+ this->downloaded_bytes += bytes;
+
+ this->SetDirty();
+ }
+};
+
+/** Window that lists the content that's at the content server */
+class NetworkContentListWindow : public Window, ContentCallback {
+ /** All widgets used */
+ enum Widgets {
+ NCLWW_CLOSE, ///< Close 'X' button
+ NCLWW_CAPTION, ///< Caption of the window
+ NCLWW_BACKGROUND, ///< Resize button
+
+ NCLWW_CHECKBOX, ///< Button above checkboxes
+ NCLWW_TYPE, ///< 'Type' button
+ NCLWW_NAME, ///< 'Name' button
+
+ NCLWW_MATRIX, ///< Panel with list of content
+ NCLWW_SCROLLBAR, ///< Scrollbar of matrix
+
+ NCLWW_DETAILS, ///< Panel with content details
+
+ NCLWW_SELECT_ALL, ///< 'Select all' button
+ NCLWW_SELECT_UPDATE, ///< 'Select updates' button
+ NCLWW_UNSELECT, ///< 'Unselect all' button
+ NCLWW_CANCEL, ///< 'Cancel' button
+ NCLWW_DOWNLOAD, ///< 'Download' button
+
+ NCLWW_RESIZE, ///< Resize button
+ };
+
+ ClientNetworkContentSocketHandler *connection; ///< Connection with the content server
+ SmallVector<ContentID, 4> requested; ///< ContentIDs we already requested (so we don't do it again)
+ ContentVector infos; ///< All content info we received
+ ContentInfo *selected; ///< The selected content info
+ int list_pos; ///< Our position in the list
+
+ /** Make sure that the currently selected content info is within the visible part of the matrix */
+ void ScrollToSelected()
+ {
+ if (this->selected == NULL) return;
+
+ if (this->list_pos < this->vscroll.pos) {
+ /* scroll up to the server */
+ this->vscroll.pos = this->list_pos;
+ } else if (this->list_pos >= this->vscroll.pos + this->vscroll.cap) {
+ /* scroll down so that the server is at the bottom */
+ this->vscroll.pos = this->list_pos - this->vscroll.cap + 1;
+ }
+ }
+
+ /**
+ * Download information of a given Content ID if not already tried
+ * @param cid the ID to try
+ */
+ void DownloadContentInfo(ContentID cid)
+ {
+ /* When we tried to download it already, don't try again */
+ if (this->requested.Contains(cid)) return;
+
+ *this->requested.Append() = cid;
+ assert(this->requested.Contains(cid));
+ this->connection->RequestContentList(1, &cid);
+ }
+
+
+ /**
+ * Get the content info based on a ContentID
+ * @param cid the ContentID to search for
+ * @return the ContentInfo or NULL if not found
+ */
+ ContentInfo *GetContent(ContentID cid)
+ {
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ ContentInfo *ci = *iter;
+ if (ci->id == cid) return ci;
+ }
+ return NULL;
+ }
+
+ /**
+ * Reverse lookup the dependencies of (direct) parents over a given child.
+ * @param parents list to store all parents in (is not cleared)
+ * @param child the child to search the parents' dependencies for
+ */
+ void ReverseLookupDependency(ContentVector &parents, ContentInfo *child)
+ {
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ ContentInfo *ci = *iter;
+ if (ci == child) continue;
+
+ for (uint i = 0; i < ci->dependency_count; i++) {
+ if (ci->dependencies[i] == child->id) {
+ *parents.Append() = ci;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Reverse lookup the dependencies of all parents over a given child.
+ * @param tree list to store all parents in (is not cleared)
+ * @param child the child to search the parents' dependencies for
+ */
+ void ReverseLookupTreeDependency(ContentVector &tree, ContentInfo *child)
+ {
+ *tree.Append() = child;
+
+ /* First find all direct parents */
+ for (ContentIterator iter = tree.Begin(); iter != tree.End(); iter++) {
+ ContentVector parents;
+ ReverseLookupDependency(parents, *iter);
+
+ for (ContentIterator piter = parents.Begin(); piter != parents.End(); piter++) {
+ tree.Include(*piter);
+ }
+ }
+ }
+
+ /**
+ * Check the dependencies (recursively) of this content info
+ * @param ci the content info to check the dependencies of
+ */
+ void CheckDependencyState(ContentInfo *ci)
+ {
+ if (ci->IsSelected() || ci->state == ContentInfo::ALREADY_HERE) {
+ /* Selection is easy; just walk all children and set the
+ * autoselected state. That way we can see what we automatically
+ * selected and thus can unselect when a dependency is removed. */
+ for (uint i = 0; i < ci->dependency_count; i++) {
+ ContentInfo *c = this->GetContent(ci->dependencies[i]);
+ if (c == NULL) {
+ DownloadContentInfo(ci->dependencies[i]);
+ } else if (c->state == ContentInfo::UNSELECTED) {
+ c->state = ContentInfo::AUTOSELECTED;
+ this->CheckDependencyState(c);
+ }
+ }
+ return;
+ }
+
+ if (ci->state != ContentInfo::UNSELECTED) return;
+
+ /* For unselection we need to find the parents of us. We need to
+ * unselect them. After that we unselect all children that we
+ * depend on and are not used as dependency for us, but only when
+ * we automatically selected them. */
+ ContentVector parents;
+ ReverseLookupDependency(parents, ci);
+ for (ContentIterator iter = parents.Begin(); iter != parents.End(); iter++) {
+ ContentInfo *c = *iter;
+ if (!c->IsSelected()) continue;
+
+ c->state = ContentInfo::UNSELECTED;
+ CheckDependencyState(c);
+ }
+
+ for (uint i = 0; i < ci->dependency_count; i++) {
+ ContentInfo *c = this->GetContent(ci->dependencies[i]);
+ if (c == NULL) {
+ DownloadContentInfo(ci->dependencies[i]);
+ continue;
+ }
+ if (c->state != ContentInfo::AUTOSELECTED) continue;
+
+ /* Only unselect when WE are the only parent. */
+ parents.Clear();
+ ReverseLookupDependency(parents, c);
+
+ /* First check whether anything depends on us */
+ int sel_count = 0;
+ bool force_selection = false;
+ for (ContentIterator iter = parents.Begin(); iter != parents.End(); iter++) {
+ if ((*iter)->IsSelected()) sel_count++;
+ if ((*iter)->state == ContentInfo::SELECTED) force_selection = true;
+ }
+ if (sel_count == 0) {
+ /* Nothing depends on us */
+ c->state = ContentInfo::UNSELECTED;
+ this->CheckDependencyState(c);
+ continue;
+ }
+ /* Something manually selected depends directly on us */
+ if (force_selection) continue;
+
+ /* "Flood" search to find all items in the dependency graph*/
+ parents.Clear();
+ ReverseLookupTreeDependency(parents, c);
+
+ /* Is there anything that is "force" selected?, if so... we're done. */
+ for (ContentIterator iter = parents.Begin(); iter != parents.End(); iter++) {
+ if ((*iter)->state != ContentInfo::SELECTED) continue;
+
+ force_selection = true;
+ break;
+ }
+
+ /* So something depended directly on us */
+ if (force_selection) continue;
+
+ /* Nothing depends on us, mark the whole graph as unselected.
+ * After that's done run over them once again to test their children
+ * to unselect. Don't do it immediatelly because it'll do exactly what
+ * we're doing now. */
+ for (ContentIterator iter = parents.Begin(); iter != parents.End(); iter++) {
+ ContentInfo *c = *iter;
+ if (c->state == ContentInfo::AUTOSELECTED) c->state = ContentInfo::UNSELECTED;
+ }
+ for (ContentIterator iter = parents.Begin(); iter != parents.End(); iter++) {
+ this->CheckDependencyState(*iter);
+ }
+ }
+ }
+
+ /** Toggle the state of a content info and check it's dependencies */
+ void ToggleSelectedState()
+ {
+ switch (this->selected->state) {
+ case ContentInfo::SELECTED:
+ case ContentInfo::AUTOSELECTED:
+ this->selected->state = ContentInfo::UNSELECTED;
+ break;
+
+ case ContentInfo::UNSELECTED:
+ this->selected->state = ContentInfo::SELECTED;
+ break;
+
+ default:
+ return;
+ }
+
+ this->CheckDependencyState(this->selected);
+ }
+
+public:
+ /**
+ * Create the content list window.
+ * @param desc the window description to pass to Window's constructor.
+ * @param cv the list with content to show; if NULL find content ourselves
+ */
+ NetworkContentListWindow(const WindowDesc *desc, ContentVector *cv, ContentType type) : Window(desc, 1), selected(NULL), list_pos(0)
+ {
+ this->connection = NetworkContent_Connect(this);
+
+ this->vscroll.cap = 14;
+ this->resize.step_height = 14;
+ this->resize.step_width = 2;
+
+ if (cv == NULL) {
+ if (type == CONTENT_TYPE_END) {
+ this->connection->RequestContentList(CONTENT_TYPE_BASE_GRAPHICS);
+ this->connection->RequestContentList(CONTENT_TYPE_NEWGRF);
+ this->connection->RequestContentList(CONTENT_TYPE_AI);
+ } else {
+ this->connection->RequestContentList(type);
+ }
+ this->HideWidget(NCLWW_SELECT_ALL);
+ } else {
+ this->connection->RequestContentList(cv, true);
+
+ for (ContentIterator iter = cv->Begin(); iter != cv->End(); iter++) {
+ *this->infos.Append() = *iter;
+ }
+ this->HideWidget(NCLWW_SELECT_UPDATE);
+ }
+
+ this->FindWindowPlacementAndResize(desc);
+ }
+
+ /** Free everything we allocated */
+ ~NetworkContentListWindow()
+ {
+ NetworkContent_Disconnect(this);
+
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) delete *iter;
+ }
+
+ virtual void OnPaint()
+ {
+ /* To sum all the bytes we intend to download */
+ uint filesize = 0;
+ bool show_select_all = false;
+ bool show_select_update = false;
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ const ContentInfo *ci = *iter;
+ switch (ci->state) {
+ case ContentInfo::SELECTED:
+ case ContentInfo::AUTOSELECTED:
+ filesize += ci->filesize;
+ break;
+
+ case ContentInfo::UNSELECTED:
+ show_select_all = true;
+ show_select_update |= ci->update;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ this->SetWidgetDisabledState(NCLWW_DOWNLOAD, filesize == 0);
+ this->SetWidgetDisabledState(NCLWW_UNSELECT, filesize == 0);
+ this->SetWidgetDisabledState(NCLWW_SELECT_ALL, !show_select_all);
+ this->SetWidgetDisabledState(NCLWW_SELECT_UPDATE, !show_select_update);
+
+ this->DrawWidgets();
+
+ /* Fill the matrix with the information */
+ uint y = this->widget[NCLWW_MATRIX].top + 3;
+ int cnt = 0;
+ for (ContentIterator iter = this->infos.Get(this->vscroll.pos); iter != this->infos.End() && cnt < this->vscroll.cap; iter++, cnt++) {
+ const ContentInfo *ci = *iter;
+
+ if (ci == this->selected) GfxFillRect(this->widget[NCLWW_CHECKBOX].left + 1, y - 2, this->widget[NCLWW_NAME].right - 1, y + 9, 10);
+
+ SpriteID sprite;
+ SpriteID pal = PAL_NONE;
+ switch (ci->state) {
+ case ContentInfo::UNSELECTED: sprite = SPR_BOX_EMPTY; break;
+ case ContentInfo::SELECTED: sprite = SPR_BOX_CHECKED; break;
+ case ContentInfo::AUTOSELECTED: sprite = SPR_BOX_CHECKED; break;
+ case ContentInfo::ALREADY_HERE: sprite = SPR_BLOT; pal = PALETTE_TO_GREEN; break;
+ case ContentInfo::DOES_NOT_EXIST: sprite = SPR_BLOT; pal = PALETTE_TO_RED; break;
+ default: NOT_REACHED();
+ }
+ DrawSprite(sprite, pal, this->widget[NCLWW_CHECKBOX].left + (pal == PAL_NONE ? 3 : 4), y + (pal == PAL_NONE ? 1 : 0));
+
+ StringID str = STR_CONTENT_TYPE_BASE_GRAPHICS + ci->type - CONTENT_TYPE_BASE_GRAPHICS;
+ DrawStringCenteredTruncated(this->widget[NCLWW_TYPE].left, this->widget[NCLWW_TYPE].right, y, str, TC_BLACK);
+
+ SetDParamStr(0, ci->name);
+ DrawStringTruncated(this->widget[NCLWW_NAME].left + 5, y, STR_JUST_RAW_STRING, TC_BLACK, this->widget[NCLWW_NAME].right - this->widget[NCLWW_NAME].left - 5);
+ y += this->resize.step_height;
+ }
+
+ /* Create the nice grayish rectangle at the details top */
+ GfxFillRect(this->widget[NCLWW_DETAILS].left + 1, this->widget[NCLWW_DETAILS].top + 1, this->widget[NCLWW_DETAILS].right - 1, this->widget[NCLWW_DETAILS].top + 50, 157);
+ DrawStringCentered((this->widget[NCLWW_DETAILS].left + this->widget[NCLWW_DETAILS].right) / 2, this->widget[NCLWW_DETAILS].top + 11, STR_CONTENT_DETAIL_TITLE, TC_FROMSTRING);
+
+ if (this->selected == NULL) return;
+
+ /* And fill the rest of the details when there's information to place there */
+ DrawStringMultiCenter((this->widget[NCLWW_DETAILS].left + this->widget[NCLWW_DETAILS].right) / 2, this->widget[NCLWW_DETAILS].top + 32, STR_CONTENT_DETAIL_SUBTITLE_UNSELECTED + this->selected->state, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 10);
+
+ /* Also show the total download size, so keep some space from the bottom */
+ const uint max_y = this->widget[NCLWW_DETAILS].bottom - 15;
+ y = this->widget[NCLWW_DETAILS].top + 55;
+
+ if (this->selected->update) {
+ SetDParam(0, STR_CONTENT_TYPE_BASE_GRAPHICS + this->selected->type - CONTENT_TYPE_BASE_GRAPHICS);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_UPDATE, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ y += 11;
+ }
+
+ SetDParamStr(0, this->selected->name);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_NAME, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+
+ if (!StrEmpty(this->selected->version)) {
+ SetDParamStr(0, this->selected->version);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_VERSION, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ }
+
+ if (!StrEmpty(this->selected->description)) {
+ SetDParamStr(0, this->selected->description);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_DESCRIPTION, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ }
+
+ if (!StrEmpty(this->selected->url)) {
+ SetDParamStr(0, this->selected->url);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_URL, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ }
+
+ SetDParam(0, STR_CONTENT_TYPE_BASE_GRAPHICS + this->selected->type - CONTENT_TYPE_BASE_GRAPHICS);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_TYPE, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+
+ y += 11;
+ SetDParam(0, this->selected->filesize);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_FILESIZE, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+
+ if (this->selected->dependency_count != 0) {
+ /* List dependencies */
+ char buf[8192] = "";
+ char *p = buf;
+ for (uint i = 0; i < this->selected->dependency_count; i++) {
+ ContentID cid = this->selected->dependencies[i];
+
+ /* Try to find the dependency */
+ ContentIterator iter = this->infos.Begin();
+ for (; iter != this->infos.End(); iter++) {
+ const ContentInfo *ci = *iter;
+ if (ci->id != cid) continue;
+
+ p += seprintf(p, lastof(buf), p == buf ? "%s" : ", %s", (*iter)->name);
+ break;
+ }
+
+ /* We miss the dependency, but we'll only request it if not done before. */
+ if (iter == this->infos.End()) DownloadContentInfo(cid);
+ }
+ SetDParamStr(0, buf);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_DEPENDENCIES, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ }
+
+ if (this->selected->tag_count != 0) {
+ /* List all tags */
+ char buf[8192] = "";
+ char *p = buf;
+ for (uint i = 0; i < this->selected->tag_count; i++) {
+ p += seprintf(p, lastof(buf), i == 0 ? "%s" : ", %s", this->selected->tags[i]);
+ }
+ SetDParamStr(0, buf);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_TAGS, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ }
+
+ if (this->selected->IsSelected()) {
+ /* When selected show all manually selected content that depends on this */
+ ContentVector tree;
+ ReverseLookupTreeDependency(tree, this->selected);
+
+ char buf[8192] = "";
+ char *p = buf;
+ for (ContentIterator iter = tree.Begin(); iter != tree.End(); iter++) {
+ ContentInfo *ci = *iter;
+ if (ci == this->selected || ci->state != ContentInfo::SELECTED) continue;
+
+ p += seprintf(p, lastof(buf), buf == p ? "%s" : ", %s", ci->name);
+ }
+ if (p != buf) {
+ SetDParamStr(0, buf);
+ y += DrawStringMultiLine(this->widget[NCLWW_DETAILS].left + 5, y, STR_CONTENT_DETAIL_SELECTED_BECAUSE_OF, this->widget[NCLWW_DETAILS].right - this->widget[NCLWW_DETAILS].left - 5, max_y - y);
+ }
+ }
+
+ /* Draw the total download size */
+ SetDParam(0, filesize);
+ DrawString(this->widget[NCLWW_DETAILS].left + 5, this->widget[NCLWW_DETAILS].bottom - 12, STR_CONTENT_TOTAL_DOWNLOAD_SIZE, TC_BLACK);
+ }
+
+ virtual void OnDoubleClick(Point pt, int widget)
+ {
+ /* Double clicking on a line in the matrix toggles the state of the checkbox */
+ if (widget != NCLWW_MATRIX) return;
+
+ pt.x = this->widget[NCLWW_CHECKBOX].left;
+ this->OnClick(pt, widget);
+ }
+
+ virtual void OnClick(Point pt, int widget)
+ {
+ switch (widget) {
+ case NCLWW_MATRIX: {
+ uint32 id_v = (pt.y - this->widget[NCLWW_MATRIX].top) / this->resize.step_height;
+
+ if (id_v >= this->vscroll.cap) return; // click out of bounds
+ id_v += this->vscroll.pos;
+
+ if (id_v >= this->infos.Length()) return; // click out of bounds
+
+ this->selected = this->infos[id_v];
+ this->list_pos = id_v;
+
+ if (pt.x <= this->widget[NCLWW_CHECKBOX].right) this->ToggleSelectedState();
+
+ this->SetDirty();
+ } break;
+
+ case NCLWW_SELECT_ALL:
+ case NCLWW_SELECT_UPDATE:
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ ContentInfo *ci = *iter;
+ if (ci->state == ContentInfo::UNSELECTED && (widget == NCLWW_SELECT_ALL || ci->update)) ci->state = ContentInfo::SELECTED;
+ }
+ this->SetDirty();
+ break;
+
+ case NCLWW_UNSELECT:
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ ContentInfo *ci = *iter;
+ if (ci->IsSelected()) ci->state = ContentInfo::UNSELECTED;
+ }
+ this->SetDirty();
+ break;
+
+ case NCLWW_CANCEL:
+ delete this;
+ break;
+
+ case NCLWW_DOWNLOAD:
+ new NetworkContentDownloadStatusWindow(this->infos);
+ break;
+ }
+ }
+
+ virtual EventState OnKeyPress(uint16 key, uint16 keycode)
+ {
+ if (this->infos.Length() == 0) return ES_HANDLED;
+
+ switch (keycode) {
+ case WKC_UP:
+ /* scroll up by one */
+ if (this->list_pos > 0) this->list_pos--;
+ break;
+ case WKC_DOWN:
+ /* scroll down by one */
+ if (this->list_pos < (int)this->infos.Length() - 1) this->list_pos++;
+ break;
+ case WKC_PAGEUP:
+ /* scroll up a page */
+ this->list_pos = (this->list_pos < this->vscroll.cap) ? 0 : this->list_pos - this->vscroll.cap;
+ break;
+ case WKC_PAGEDOWN:
+ /* scroll down a page */
+ this->list_pos = min(this->list_pos + this->vscroll.cap, (int)this->infos.Length() - 1);
+ break;
+ case WKC_HOME:
+ /* jump to beginning */
+ this->list_pos = 0;
+ break;
+ case WKC_END:
+ /* jump to end */
+ this->list_pos = this->infos.Length() - 1;
+ break;
+
+ case WKC_SPACE:
+ this->ToggleSelectedState();
+ this->SetDirty();
+ return ES_HANDLED;
+
+ default: return ES_NOT_HANDLED;
+ }
+
+ this->selected = this->infos[this->list_pos];
+
+ /* scroll to the new server if it is outside the current range */
+ this->ScrollToSelected();
+
+ /* redraw window */
+ this->SetDirty();
+ return ES_HANDLED;
+ }
+
+ virtual void OnResize(Point new_size, Point delta)
+ {
+ this->vscroll.cap += delta.y / (int)this->resize.step_height;
+
+ this->widget[NCLWW_MATRIX].data = (this->vscroll.cap << 8) + 1;
+
+ SetVScrollCount(this, this->infos.Length());
+
+ /* Make the matrix and details section grow both bigger (or smaller) */
+ delta.x /= 2;
+ this->widget[NCLWW_NAME].right -= delta.x;
+ this->widget[NCLWW_MATRIX].right -= delta.x;
+ this->widget[NCLWW_SCROLLBAR].left -= delta.x;
+ this->widget[NCLWW_SCROLLBAR].right -= delta.x;
+ this->widget[NCLWW_DETAILS].left -= delta.x;
+ }
+
+ virtual void OnReceiveContentInfo(ContentInfo *rci)
+ {
+ /* Do we already have a stub for this? */
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ ContentInfo *ci = *iter;
+ if (ci->type == rci->type && ci->unique_id == rci->unique_id &&
+ memcmp(ci->md5sum, rci->md5sum, sizeof(ci->md5sum)) == 0) {
+ /* Preserve the name if possible */
+ if (StrEmpty(rci->name)) strecpy(rci->name, ci->name, lastof(rci->name));
+
+ delete ci;
+ *iter = rci;
+
+ this->SetDirty();
+ return;
+ }
+ }
+
+ /* Missing content info? Don't list it */
+ if (rci->filesize == 0) {
+ delete rci;
+ return;
+ }
+
+ /* Nothing selected, lets select something */
+ if (this->selected == NULL) this->selected = rci;
+
+ *this->infos.Append() = rci;
+
+ /* Incoming data means that we might need to reconsider dependencies */
+ for (ContentIterator iter = this->infos.Begin(); iter != this->infos.End(); iter++) {
+ CheckDependencyState(*iter);
+ }
+
+ this->SetDirty();
+ }
+
+ virtual void OnDownloadComplete(ContentID cid)
+ {
+ /* When we know something that completed downloading, we have it! */
+ ContentInfo *ci = this->GetContent(cid);
+ if (ci != NULL) {
+ ci->state = ContentInfo::ALREADY_HERE;
+ this->SetDirty();
+ }
+ }
+};
+
+/** Widgets used for the content list */
+static const Widget _network_content_list_widgets[] = {
+/* TOP */
+{ WWT_CLOSEBOX, RESIZE_NONE, COLOUR_LIGHT_BLUE, 0, 10, 0, 13, STR_00C5, STR_018B_CLOSE_WINDOW}, // NCLWW_CLOSE
+{ WWT_CAPTION, RESIZE_RIGHT, COLOUR_LIGHT_BLUE, 11, 449, 0, 13, STR_CONTENT_TITLE, STR_NULL}, // NCLWW_CAPTION
+{ WWT_PANEL, RESIZE_RB, COLOUR_LIGHT_BLUE, 0, 449, 14, 263, 0x0, STR_NULL}, // NCLWW_BACKGROUND
+
+/* LEFT SIDE */
+{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_WHITE, 8, 20, 22, 33, STR_EMPTY, STR_NULL}, // NCLWW_CHECKBOX
+{ WWT_PUSHTXTBTN, RESIZE_NONE, COLOUR_WHITE, 21, 110, 22, 33, STR_CONTENT_TYPE_CAPTION, STR_CONTENT_TYPE_CAPTION_TIP}, // NCLWW_TYPE
+{ WWT_PUSHTXTBTN, RESIZE_RIGHT, COLOUR_WHITE, 111, 191, 22, 33, STR_CONTENT_NAME_CAPTION, STR_CONTENT_NAME_CAPTION_TIP}, // NCLWW_NAME
+
+{ WWT_MATRIX, RESIZE_RB, COLOUR_LIGHT_BLUE, 8, 190, 34, 230, (14 << 8) | 1, STR_NETWORK_CLICK_GAME_TO_SELECT}, // NCLWW_MATRIX
+{ WWT_SCROLLBAR, RESIZE_LRB, COLOUR_LIGHT_BLUE, 191, 202, 22, 230, 0x0, STR_0190_SCROLL_BAR_SCROLLS_LIST}, // NCLWW_SCROLLBAR
+
+/* RIGHT SIDE */
+{ WWT_PANEL, RESIZE_LRB, COLOUR_LIGHT_BLUE, 210, 440, 22, 230, 0x0, STR_NULL}, // NCLWW_DETAILS
+
+/* BOTTOM */
+{ WWT_PUSHTXTBTN, RESIZE_TB, COLOUR_WHITE, 10, 110, 238, 249, STR_CONTENT_SELECT_ALL_CAPTION, STR_CONTENT_SELECT_ALL_CAPTION_TIP}, // NCLWW_SELECT_ALL
+{ WWT_PUSHTXTBTN, RESIZE_TB, COLOUR_WHITE, 10, 110, 238, 249, STR_CONTENT_SELECT_UPDATES_CAPTION, STR_CONTENT_SELECT_UPDATES_CAPTION_TIP}, // NCLWW_SELECT_UPDATES
+{ WWT_PUSHTXTBTN, RESIZE_TB, COLOUR_WHITE, 118, 218, 238, 249, STR_CONTENT_UNSELECT_ALL_CAPTION, STR_CONTENT_UNSELECT_ALL_CAPTION_TIP}, // NCLWW_UNSELECT
+{ WWT_PUSHTXTBTN, RESIZE_LRTB, COLOUR_WHITE, 226, 326, 238, 249, STR_012E_CANCEL, STR_NULL}, // NCLWW_CANCEL
+{ WWT_PUSHTXTBTN, RESIZE_LRTB, COLOUR_WHITE, 334, 434, 238, 249, STR_CONTENT_DOWNLOAD_CAPTION, STR_CONTENT_DOWNLOAD_CAPTION_TIP}, // NCLWW_DOWNLOAD
+
+{ WWT_RESIZEBOX, RESIZE_LRTB, COLOUR_LIGHT_BLUE, 438, 449, 252, 263, 0x0, STR_RESIZE_BUTTON }, // NCLWW_RESIZE
+
+{ WIDGETS_END},
+};
+
+/** Window description of the content list */
+static const WindowDesc _network_content_list_desc = {
+ WDP_CENTER, WDP_CENTER, 450, 264, 630, 460,
+ WC_NETWORK_WINDOW, WC_NONE,
+ WDF_STD_TOOLTIPS | WDF_DEF_WIDGET | WDF_STD_BTN | WDF_UNCLICK_BUTTONS | WDF_RESIZABLE,
+ _network_content_list_widgets,
+};
+
+/**
+ * Show the content list window with a given set of content
+ * @param cv the content to show, or NULL when it has to search for itself
+ * @param type the type to (only) show
+ */
+void ShowNetworkContentListWindow(ContentVector *cv, ContentType type)
+{
+#if defined(WITH_ZLIB)
+ if (NetworkContent_Connect(NULL)) {
+ DeleteWindowById(WC_NETWORK_WINDOW, 1);
+ new NetworkContentListWindow(&_network_content_list_desc, cv, type);
+ NetworkContent_Disconnect(NULL);
+ } else {
+ ShowErrorMessage(INVALID_STRING_ID, STR_CONTENT_ERROR_COULD_NOT_CONNECT, 0, 0);
+#else
+ {
+ ShowErrorMessage(STR_CONTENT_NO_ZLIB_SUB, STR_CONTENT_NO_ZLIB, 0, 0);
+#endif /* WITH_ZLIB */
+ /* Connection failed... clean up the mess */
+ if (cv != NULL) {
+ for (ContentIterator iter = cv->Begin(); iter != cv->End(); iter++) delete *iter;
+ }
+ }
+}
+
+#endif /* ENABLE_NETWORK */
diff --git a/src/network/network_gui.cpp b/src/network/network_gui.cpp
index e130b7770..f9b0b7cc9 100644
--- a/src/network/network_gui.cpp
+++ b/src/network/network_gui.cpp
@@ -11,6 +11,7 @@
#include "../fios.h"
#include "network_internal.h"
#include "network_client.h"
+#include "network_content.h"
#include "network_gui.h"
#include "network_gamelist.h"
#include "../gui.h"
@@ -76,7 +77,7 @@ enum {
* @param unselect unselect the currently selected item */
void UpdateNetworkGameWindow(bool unselect)
{
- InvalidateWindowData(WC_NETWORK_WINDOW, 0, unselect);
+ InvalidateWindowData(WC_NETWORK_WINDOW, 0, unselect ? 1 : 0);
}
/** Enum for NetworkGameWindow, referring to _network_game_window_widgets */
@@ -595,9 +596,36 @@ public:
virtual void OnInvalidateData(int data)
{
- if (data != 0) {
- this->server = NULL;
- this->list_pos = SLP_INVALID;
+ switch (data) {
+ /* Remove the selection */
+ case 1:
+ this->server = NULL;
+ this->list_pos = SLP_INVALID;
+ break;
+
+ /* Reiterate the whole server list as we downloaded some files */
+ case 2:
+ for (NetworkGameList **iter = this->servers.Begin(); iter != this->servers.End(); iter++) {
+ NetworkGameList *item = *iter;
+ bool missing_grfs = false;
+ for (GRFConfig *c = item->info.grfconfig; c != NULL; c = c->next) {
+ if (c->status != GCS_NOT_FOUND) continue;
+
+ const GRFConfig *f = FindGRFConfig(c->grfid, c->md5sum);
+ if (f == NULL) {
+ missing_grfs = true;
+ continue;
+ }
+
+ c->filename = f->filename;
+ c->name = f->name;
+ c->info = f->info;
+ c->status = GCS_UNKNOWN;
+ }
+
+ if (!missing_grfs) item->info.compatible = item->info.version_compatible;
+ }
+ break;
}
this->servers.ForceRebuild();
this->SetDirty();
diff --git a/src/newgrf_gui.cpp b/src/newgrf_gui.cpp
index 17863d78a..cb8bd89f1 100644
--- a/src/newgrf_gui.cpp
+++ b/src/newgrf_gui.cpp
@@ -18,6 +18,8 @@
#include "gamelog.h"
#include "settings_func.h"
#include "widgets/dropdown_type.h"
+#include "network/network.h"
+#include "network/network_content.h"
#include "table/strings.h"
#include "table/sprites.h"
@@ -307,6 +309,7 @@ struct NewGRFWindow : public Window {
SNGRFS_SET_PARAMETERS,
SNGRFS_TOGGLE_PALETTE,
SNGRFS_APPLY_CHANGES,
+ SNGRFS_CONTENT_DOWNLOAD,
SNGRFS_RESIZE,
};
@@ -579,13 +582,34 @@ struct NewGRFWindow : public Window {
break;
}
- case SNGRFS_TOGGLE_PALETTE: {
+ case SNGRFS_TOGGLE_PALETTE:
if (this->sel != NULL) {
this->sel->windows_paletted ^= true;
this->SetDirty();
}
break;
- }
+
+ case SNGRFS_CONTENT_DOWNLOAD:
+ if (!_network_available) {
+ ShowErrorMessage(INVALID_STRING_ID, STR_NETWORK_ERR_NOTAVAILABLE, 0, 0);
+ } else {
+#if defined(ENABLE_NETWORK)
+ /* Only show the things in the current list, or everything when nothing's selected */
+ ContentVector cv;
+ for (const GRFConfig *c = this->list; c != NULL; c = c->next) {
+ ContentInfo *ci = new ContentInfo();
+ ci->type = CONTENT_TYPE_NEWGRF;
+ ci->state = ContentInfo::DOES_NOT_EXIST;
+ ttd_strlcpy(ci->name, c->name, lengthof(ci->name));
+ ci->unique_id = BSWAP32(c->grfid);
+ memcpy(ci->md5sum, c->md5sum, sizeof(ci->md5sum));
+ *cv.Append() = ci;
+ }
+ ShowNetworkContentListWindow(cv.Length() == 0 ? NULL : &cv, CONTENT_TYPE_NEWGRF);
+#endif
+ }
+ break;
+
}
}
@@ -658,10 +682,34 @@ struct NewGRFWindow : public Window {
this->SetupNewGRFWindow();
}
- virtual void OnInvalidateData(int data = 0)
+ virtual void OnInvalidateData(int data)
{
- this->preset = -1;
- this->SetupNewGRFWindow();
+ switch (data) {
+ default: NOT_REACHED();
+ case 0:
+ this->preset = -1;
+ this->SetupNewGRFWindow();
+ break;
+
+ case 1:
+ /* Search the list for items that are now found and mark them as such. */
+ for (GRFConfig *c = this->list; c != NULL; c = c->next) {
+ if (c->status != GCS_NOT_FOUND) continue;
+
+ const GRFConfig *f = FindGRFConfig(c->grfid, c->md5sum);
+ if (f == NULL) continue;
+
+ free(c->filename);
+ free(c->name);
+ free(c->info);
+
+ c->filename = f->filename == NULL ? NULL : strdup(f->filename);
+ c->name = f->name == NULL ? NULL : strdup(f->name);;
+ c->info = f->info == NULL ? NULL : strdup(f->info);;
+ c->status = GCS_UNKNOWN;
+ }
+ break;
+ }
}
};
@@ -681,16 +729,17 @@ static const Widget _newgrf_widgets[] = {
{ WWT_MATRIX, RESIZE_RB, COLOUR_MAUVE, 0, 287, 46, 115, 0x501, STR_NEWGRF_FILE_TIP }, // SNGRFS_FILE_LIST
{ WWT_SCROLLBAR, RESIZE_LRB, COLOUR_MAUVE, 288, 299, 46, 115, 0x0, STR_0190_SCROLL_BAR_SCROLLS_LIST }, // SNGRFS_SCROLLBAR
{ WWT_PANEL, RESIZE_RTB, COLOUR_MAUVE, 0, 299, 116, 238, STR_NULL, STR_NULL }, // SNGRFS_NEWGRF_INFO
-{ WWT_PUSHTXTBTN, RESIZE_TB, COLOUR_MAUVE, 0, 95, 239, 250, STR_NEWGRF_SET_PARAMETERS, STR_NULL }, // SNGRFS_SET_PARAMETERS
-{ WWT_PUSHTXTBTN, RESIZE_RTB, COLOUR_MAUVE, 96, 191, 239, 250, STR_NEWGRF_TOGGLE_PALETTE, STR_NEWGRF_TOGGLE_PALETTE_TIP }, // SNGRFS_TOGGLE_PALETTE
-{ WWT_PUSHTXTBTN, RESIZE_RTB, COLOUR_MAUVE, 192, 287, 239, 250, STR_NEWGRF_APPLY_CHANGES, STR_NULL }, // SNGRFS_APPLY_CHANGES
-{ WWT_RESIZEBOX, RESIZE_LRTB, COLOUR_MAUVE, 288, 299, 239, 250, 0x0, STR_RESIZE_BUTTON }, // SNGRFS_RESIZE
+{ WWT_PUSHTXTBTN, RESIZE_TB, COLOUR_MAUVE, 0, 99, 239, 250, STR_NEWGRF_SET_PARAMETERS, STR_NULL }, // SNGRFS_SET_PARAMETERS
+{ WWT_PUSHTXTBTN, RESIZE_RTB, COLOUR_MAUVE, 100, 199, 239, 250, STR_NEWGRF_TOGGLE_PALETTE, STR_NEWGRF_TOGGLE_PALETTE_TIP }, // SNGRFS_TOGGLE_PALETTE
+{ WWT_PUSHTXTBTN, RESIZE_RTB, COLOUR_MAUVE, 200, 299, 239, 250, STR_NEWGRF_APPLY_CHANGES, STR_NULL }, // SNGRFS_APPLY_CHANGES
+{ WWT_PUSHTXTBTN, RESIZE_RTB, COLOUR_MAUVE, 0, 287, 251, 262, STR_CONTENT_INTRO_BUTTON, STR_CONTENT_INTRO_BUTTON_TIP }, // SNGRFS_DOWNLOAD_CONTENT
+{ WWT_RESIZEBOX, RESIZE_LRTB, COLOUR_MAUVE, 288, 299, 251, 261, 0x0, STR_RESIZE_BUTTON }, // SNGRFS_RESIZE
{ WIDGETS_END },
};
/* Window definition of the manage newgrfs window */
static const WindowDesc _newgrf_desc = {
- WDP_CENTER, WDP_CENTER, 300, 251, 300, 251,
+ WDP_CENTER, WDP_CENTER, 300, 262, 300, 262,
WC_GAME_OPTIONS, WC_NONE,
WDF_STD_TOOLTIPS | WDF_STD_BTN | WDF_DEF_WIDGET | WDF_UNCLICK_BUTTONS | WDF_RESIZABLE,
_newgrf_widgets,