summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrubidium <rubidium@openttd.org>2009-01-17 16:53:32 +0000
committerrubidium <rubidium@openttd.org>2009-01-17 16:53:32 +0000
commit3a13b75e37b5642de3c1e89cf6ab3bf860b76375 (patch)
tree76215ba6e27bc0b1f49919c01ff2608f276b8e3d
parent2850bf9e006ebdd40b5562cca5117bca027cfab5 (diff)
downloadopenttd-3a13b75e37b5642de3c1e89cf6ab3bf860b76375.tar.xz
(svn r15126) -Feature: downloading content from a central server (content.openttd.org) where authors can upload they NewGRFS/AI etc. This should make joining servers that use only NewGRFs that are distributed via this system easier as the players can download the NewGRFs from in the game. It should also make it easier to see whether there are updates for NewGRFs and make the necessary updates.
-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,