aboutsummaryrefslogtreecommitdiff
path: root/builtin/mainmenu/tab_mods.lua
blob: 9510a9e18a7fa3028c6c561d2d3be4ae9083472b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
--Minetest
--Copyright (C) 2014 sapier
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

--------------------------------------------------------------------------------
local function get_formspec(tabview, name, tabdata)

	if modmgr.global_mods == nil then
		modmgr.refresh_globals()
	end

	if tabdata.selected_mod == nil then
		tabdata.selected_mod = 1
	end

	local retval =
		"label[0.05,-0.25;".. fgettext("Installed Mods:") .. "]" ..
		"tablecolumns[color;tree;text]" ..
		"table[0,0.25;5.1,5;modlist;" ..
		modmgr.render_modlist(modmgr.global_mods) ..
		";" .. tabdata.selected_mod .. "]"

	retval = retval ..
--		"label[0.8,4.2;" .. fgettext("Add mod:") .. "]" ..
--		TODO Disabled due to upcoming release 0.4.8 and irrlicht messing up localization
--		"button[0.75,4.85;1.8,0.5;btn_mod_mgr_install_local;".. fgettext("Local install") .. "]" ..

--		TODO Disabled due to service being offline, and not likely to come online again, in this form
--		"button[0,4.85;5.25,0.5;btn_modstore;".. fgettext("Online mod repository") .. "]"
		""

	local selected_mod = nil

	if filterlist.size(modmgr.global_mods) >= tabdata.selected_mod then
		selected_mod = modmgr.global_mods:get_list()[tabdata.selected_mod]
	end

	if selected_mod ~= nil then
		local modscreenshot = nil

		--check for screenshot beeing available
		local screenshotfilename = selected_mod.path .. DIR_DELIM .. "screenshot.png"
		local error = nil
		local screenshotfile,error = io.open(screenshotfilename,"r")
		if error == nil then
			screenshotfile:close()
			modscreenshot = screenshotfilename
		end

		if modscreenshot == nil then
				modscreenshot = defaulttexturedir .. "no_screenshot.png"
		end

		retval = retval
				.. "image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]"
				.. "label[8.25,0.6;" .. selected_mod.name .. "]"

		local descriptionlines = nil
		error = nil
		local descriptionfilename = selected_mod.path .. "description.txt"
		local descriptionfile,error = io.open(descriptionfilename,"r")
		if error == nil then
			local descriptiontext = descriptionfile:read("*all")

			descriptionlines = core.wrap_text(descriptiontext, 42)
			descriptionfile:close()
		else
			descriptionlines = {}
			descriptionlines[#descriptionlines + 1] = fgettext("No mod description available")
		end

		retval = retval ..
			"label[5.5,1.7;".. fgettext("Mod information:") .. "]" ..
			"textlist[5.5,2.2;6.2,2.4;description;"

		for i=1,#descriptionlines,1 do
			retval = retval .. core.formspec_escape(descriptionlines[i]) .. ","
		end


		if selected_mod.is_modpack then
			retval = retval .. ";0]" ..
				"button[10,4.85;2,0.5;btn_mod_mgr_rename_modpack;" ..
				fgettext("Rename") .. "]"
			retval = retval .. "button[5.5,4.85;4.5,0.5;btn_mod_mgr_delete_mod;"
				.. fgettext("Uninstall selected modpack") .. "]"
		else
			--show dependencies
			local toadd_hard, toadd_soft = modmgr.get_dependencies(selected_mod.path)
			if toadd_hard == "" and toadd_soft == "" then
				retval = retval .. "," .. fgettext("No dependencies.")
			else
				if toadd_hard ~= "" then
					retval = retval .. "," .. fgettext("Dependencies:") .. ","
					retval = retval .. toadd_hard
				end
				if toadd_soft ~= "" then
					if toadd_hard ~= "" then
						retval = retval .. ","
					end
					retval = retval .. "," .. fgettext("Optional dependencies:") .. ","
					retval = retval .. toadd_soft
				end
			end

			retval = retval .. ";0]"

			retval = retval .. "button[5.5,4.85;4.5,0.5;btn_mod_mgr_delete_mod;"
				.. fgettext("Uninstall selected mod") .. "]"
		end
	end
	return retval
end

--------------------------------------------------------------------------------
local function handle_buttons(tabview, fields, tabname, tabdata)
	if fields["modlist"] ~= nil then
		local event = core.explode_table_event(fields["modlist"])
		tabdata.selected_mod = event.row
		return true
	end

	if fields["btn_mod_mgr_install_local"] ~= nil then
		core.show_file_open_dialog("mod_mgt_open_dlg",fgettext("Select Mod File:"))
		return true
	end

	if fields["btn_modstore"] ~= nil then
		local modstore_ui = ui.find_by_name("modstore")
		if modstore_ui ~= nil then
			tabview:hide()
			modstore.update_modlist()
			modstore_ui:show()
		else
			print("modstore ui element not found")
		end
		return true
	end

	if fields["btn_mod_mgr_rename_modpack"] ~= nil then
		local dlg_renamemp = create_rename_modpack_dlg(tabdata.selected_mod)
		dlg_renamemp:set_parent(tabview)
		tabview:hide()
		dlg_renamemp:show()
		return true
	end

	if fields["btn_mod_mgr_delete_mod"] ~= nil then
		local dlg_delmod = create_delete_mod_dlg(tabdata.selected_mod)
		dlg_delmod:set_parent(tabview)
		tabview:hide()
		dlg_delmod:show()
		return true
	end

	if fields["mod_mgt_open_dlg_accepted"] ~= nil and
		fields["mod_mgt_open_dlg_accepted"] ~= "" then
		modmgr.installmod(fields["mod_mgt_open_dlg_accepted"],nil)
		return true
	end

	return false
end

--------------------------------------------------------------------------------
return {
	name = "mods",
	caption = fgettext("Mods"),
	cbf_formspec = get_formspec,
	cbf_button_handler = handle_buttons,
	on_change = gamemgr.update_gamelist
}
5, Perttu Ahola <celeron55@gmail.com> This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "clientmedia.h" #include "httpfetch.h" #include "client.h" #include "filecache.h" #include "filesys.h" #include "log.h" #include "porting.h" #include "settings.h" #include "util/hex.h" #include "util/serialize.h" #include "util/sha1.h" #include "util/string.h" static std::string getMediaCacheDir() { return porting::path_cache + DIR_DELIM + "media"; } bool clientMediaUpdateCache(const std::string &raw_hash, const std::string &filedata) { FileCache media_cache(getMediaCacheDir()); std::string sha1_hex = hex_encode(raw_hash); if (!media_cache.exists(sha1_hex)) return media_cache.update(sha1_hex, filedata); return true; } /* ClientMediaDownloader */ ClientMediaDownloader::ClientMediaDownloader(): m_httpfetch_caller(HTTPFETCH_DISCARD) { } ClientMediaDownloader::~ClientMediaDownloader() { if (m_httpfetch_caller != HTTPFETCH_DISCARD) httpfetch_caller_free(m_httpfetch_caller); for (auto &file_it : m_files) delete file_it.second; for (auto &remote : m_remotes) delete remote; } bool ClientMediaDownloader::loadMedia(Client *client, const std::string &data, const std::string &name) { return client->loadMedia(data, name); } void ClientMediaDownloader::addFile(const std::string &name, const std::string &sha1) { assert(!m_initial_step_done); // pre-condition // if name was already announced, ignore the new announcement if (m_files.count(name) != 0) { errorstream << "Client: ignoring duplicate media announcement " << "sent by server: \"" << name << "\"" << std::endl; return; } // if name is empty or contains illegal characters, ignore the file if (name.empty() || !string_allowed(name, TEXTURENAME_ALLOWED_CHARS)) { errorstream << "Client: ignoring illegal file name " << "sent by server: \"" << name << "\"" << std::endl; return; } // length of sha1 must be exactly 20 (160 bits), else ignore the file if (sha1.size() != 20) { errorstream << "Client: ignoring illegal SHA1 sent by server: " << hex_encode(sha1) << " \"" << name << "\"" << std::endl; return; } FileStatus *filestatus = new FileStatus(); filestatus->received = false; filestatus->sha1 = sha1; filestatus->current_remote = -1; m_files.insert(std::make_pair(name, filestatus)); } void ClientMediaDownloader::addRemoteServer(const std::string &baseurl) { assert(!m_initial_step_done); // pre-condition #ifdef USE_CURL if (g_settings->getBool("enable_remote_media_server")) { infostream << "Client: Adding remote server \"" << baseurl << "\" for media download" << std::endl; RemoteServerStatus *remote = new RemoteServerStatus(); remote->baseurl = baseurl; remote->active_count = 0; m_remotes.push_back(remote); } #else infostream << "Client: Ignoring remote server \"" << baseurl << "\" because cURL support is not compiled in" << std::endl; #endif } void ClientMediaDownloader::step(Client *client) { if (!m_initial_step_done) { initialStep(client); m_initial_step_done = true; } // Remote media: check for completion of fetches if (m_httpfetch_active) { bool fetched_something = false; HTTPFetchResult fetch_result; while (httpfetch_async_get(m_httpfetch_caller, fetch_result)) { m_httpfetch_active--; fetched_something = true; // Is this a hashset (index.mth) or a media file? if (fetch_result.request_id < m_remotes.size()) remoteHashSetReceived(fetch_result); else remoteMediaReceived(fetch_result, client); } if (fetched_something) startRemoteMediaTransfers(); // Did all remote transfers end and no new ones can be started? // If so, request still missing files from the minetest server // (Or report that we have all files.) if (m_httpfetch_active == 0) { if (m_uncached_received_count < m_uncached_count) { infostream << "Client: Failed to remote-fetch " << (m_uncached_count-m_uncached_received_count) << " files. Requesting them" << " the usual way." << std::endl; } startConventionalTransfers(client); } } } void ClientMediaDownloader::initialStep(Client *client) { // Check media cache m_uncached_count = m_files.size(); for (auto &file_it : m_files) { const std::string &name = file_it.first; FileStatus *filestatus = file_it.second; const std::string &sha1 = filestatus->sha1; if (tryLoadFromCache(name, sha1, client)) { filestatus->received = true; m_uncached_count--; } } assert(m_uncached_received_count == 0); // Create the media cache dir if we are likely to write to it if (m_uncached_count != 0) createCacheDirs(); // If we found all files in the cache, report this fact to the server. // If the server reported no remote servers, immediately start // conventional transfers. Note: if cURL support is not compiled in, // m_remotes is always empty, so "!USE_CURL" is redundant but may // reduce the size of the compiled code if (!USE_CURL || m_uncached_count == 0 || m_remotes.empty()) { startConventionalTransfers(client); } else { // Otherwise start off by requesting each server's sha1 set // This is the first time we use httpfetch, so alloc a caller ID m_httpfetch_caller = httpfetch_caller_alloc(); // Set the active fetch limit to curl_parallel_limit or 84, // whichever is greater. This gives us some leeway so that // inefficiencies in communicating with the httpfetch thread // don't slow down fetches too much. (We still want some limit // so that when the first remote server returns its hash set, // not all files are requested from that server immediately.) // One such inefficiency is that ClientMediaDownloader::step() // is only called a couple times per second, while httpfetch // might return responses much faster than that. // Note that httpfetch strictly enforces curl_parallel_limit // but at no inter-thread communication cost. This however // doesn't help with the aforementioned inefficiencies. // The signifance of 84 is that it is 2*6*9 in base 13. m_httpfetch_active_limit = g_settings->getS32("curl_parallel_limit"); m_httpfetch_active_limit = MYMAX(m_httpfetch_active_limit, 84); // Write a list of hashes that we need. This will be POSTed // to the server using Content-Type: application/octet-stream std::string required_hash_set = serializeRequiredHashSet(); // minor fixme: this loop ignores m_httpfetch_active_limit // another minor fixme, unlikely to matter in normal usage: // these index.mth fetches do (however) count against // m_httpfetch_active_limit when starting actual media file // requests, so if there are lots of remote servers that are // not responding, those will stall new media file transfers. for (u32 i = 0; i < m_remotes.size(); ++i) { assert(m_httpfetch_next_id == i); RemoteServerStatus *remote = m_remotes[i]; actionstream << "Client: Contacting remote server \"" << remote->baseurl << "\"" << std::endl; HTTPFetchRequest fetch_request; fetch_request.url = remote->baseurl + MTHASHSET_FILE_NAME; fetch_request.caller = m_httpfetch_caller; fetch_request.request_id = m_httpfetch_next_id; // == i fetch_request.method = HTTP_POST; fetch_request.raw_data = required_hash_set; fetch_request.extra_headers.emplace_back( "Content-Type: application/octet-stream"); // Encapsulate possible IPv6 plain address in [] std::string addr = client->getAddressName(); if (addr.find(':', 0) != std::string::npos) addr = '[' + addr + ']'; fetch_request.extra_headers.emplace_back( std::string("Referer: minetest://") + addr + ":" + std::to_string(client->getServerAddress().getPort())); httpfetch_async(fetch_request); m_httpfetch_active++; m_httpfetch_next_id++; m_outstanding_hash_sets++; } } } void ClientMediaDownloader::remoteHashSetReceived( const HTTPFetchResult &fetch_result) { u32 remote_id = fetch_result.request_id; assert(remote_id < m_remotes.size()); RemoteServerStatus *remote = m_remotes[remote_id]; m_outstanding_hash_sets--; if (fetch_result.succeeded) { try { // Server sent a list of file hashes that are // available on it, try to parse the list std::set<std::string> sha1_set; deSerializeHashSet(fetch_result.data, sha1_set); // Parsing succeeded: For every file that is // available on this server, add this server // to the available_remotes array for(auto it = m_files.upper_bound(m_name_bound); it != m_files.end(); ++it) { FileStatus *f = it->second; if (!f->received && sha1_set.count(f->sha1)) f->available_remotes.push_back(remote_id); } } catch (SerializationError &e) { infostream << "Client: Remote server \"" << remote->baseurl << "\" sent invalid hash set: " << e.what() << std::endl; } } } void ClientMediaDownloader::remoteMediaReceived( const HTTPFetchResult &fetch_result, Client *client) { // Some remote server sent us a file. // -> decrement number of active fetches // -> mark file as received if fetch succeeded // -> try to load media std::string name; { auto it = m_remote_file_transfers.find(fetch_result.request_id); assert(it != m_remote_file_transfers.end()); name = it->second; m_remote_file_transfers.erase(it); } sanity_check(m_files.count(name) != 0); FileStatus *filestatus = m_files[name]; sanity_check(!filestatus->received); sanity_check(filestatus->current_remote >= 0); RemoteServerStatus *remote = m_remotes[filestatus->current_remote]; filestatus->current_remote = -1; remote->active_count--; // If fetch succeeded, try to load media file if (fetch_result.succeeded) { bool success = checkAndLoad(name, filestatus->sha1, fetch_result.data, false, client); if (success) { filestatus->received = true; assert(m_uncached_received_count < m_uncached_count); m_uncached_received_count++; } } } s32 ClientMediaDownloader::selectRemoteServer(FileStatus *filestatus) { // Pre-conditions assert(filestatus != NULL); assert(!filestatus->received); assert(filestatus->current_remote < 0); if (filestatus->available_remotes.empty()) return -1; // Of all servers that claim to provide the file (and haven't // been unsuccessfully tried before), find the one with the // smallest number of currently active transfers s32 best = 0; s32 best_remote_id = filestatus->available_remotes[best]; s32 best_active_count = m_remotes[best_remote_id]->active_count; for (u32 i = 1; i < filestatus->available_remotes.size(); ++i) { s32 remote_id = filestatus->available_remotes[i]; s32 active_count = m_remotes[remote_id]->active_count; if (active_count < best_active_count) { best = i; best_remote_id = remote_id; best_active_count = active_count; } } filestatus->available_remotes.erase( filestatus->available_remotes.begin() + best); return best_remote_id; } void ClientMediaDownloader::startRemoteMediaTransfers() { bool changing_name_bound = true; for (auto files_iter = m_files.upper_bound(m_name_bound); files_iter != m_files.end(); ++files_iter) { // Abort if active fetch limit is exceeded if (m_httpfetch_active >= m_httpfetch_active_limit) break; const std::string &name = files_iter->first; FileStatus *filestatus = files_iter->second; if (!filestatus->received && filestatus->current_remote < 0) { // File has not been received yet and is not currently // being transferred. Choose a server for it. s32 remote_id = selectRemoteServer(filestatus); if (remote_id >= 0) { // Found a server, so start fetching RemoteServerStatus *remote = m_remotes[remote_id]; std::string url = remote->baseurl + hex_encode(filestatus->sha1); verbosestream << "Client: " << "Requesting remote media file " << "\"" << name << "\" " << "\"" << url << "\"" << std::endl; HTTPFetchRequest fetch_request; fetch_request.url = url; fetch_request.caller = m_httpfetch_caller; fetch_request.request_id = m_httpfetch_next_id; fetch_request.timeout = g_settings->getS32("curl_file_download_timeout"); httpfetch_async(fetch_request); m_remote_file_transfers.insert(std::make_pair( m_httpfetch_next_id, name)); filestatus->current_remote = remote_id; remote->active_count++; m_httpfetch_active++; m_httpfetch_next_id++; } } if (filestatus->received || (filestatus->current_remote < 0 && !m_outstanding_hash_sets)) { // If we arrive here, we conclusively know that we // won't fetch this file from a remote server in the // future. So update the name bound if possible. if (changing_name_bound) m_name_bound = name; } else changing_name_bound = false; } } void ClientMediaDownloader::startConventionalTransfers(Client *client) { assert(m_httpfetch_active == 0); // pre-condition if (m_uncached_received_count != m_uncached_count) { // Some media files have not been received yet, use the // conventional slow method (minetest protocol) to get them std::vector<std::string> file_requests; for (auto &file : m_files) { if (!file.second->received) file_requests.push_back(file.first); } assert((s32) file_requests.size() == m_uncached_count - m_uncached_received_count); client->request_media(file_requests); } } bool ClientMediaDownloader::conventionalTransferDone( const std::string &name, const std::string &data, Client *client) { // Check that file was announced auto file_iter = m_files.find(name); if (file_iter == m_files.end()) { errorstream << "Client: server sent media file that was" << "not announced, ignoring it: \"" << name << "\"" << std::endl; return false; } FileStatus *filestatus = file_iter->second; assert(filestatus != NULL); // Check that file hasn't already been received if (filestatus->received) { errorstream << "Client: server sent media file that we already" << "received, ignoring it: \"" << name << "\"" << std::endl; return true; } // Mark file as received, regardless of whether loading it works and // whether the checksum matches (because at this point there is no // other server that could send a replacement) filestatus->received = true; assert(m_uncached_received_count < m_uncached_count); m_uncached_received_count++; // Check that received file matches announced checksum // If so, load it checkAndLoad(name, filestatus->sha1, data, false, client); return true; } /* IClientMediaDownloader */ IClientMediaDownloader::IClientMediaDownloader(): m_media_cache(getMediaCacheDir()), m_write_to_cache(true) { } void IClientMediaDownloader::createCacheDirs() { if (!m_write_to_cache) return; std::string path = getMediaCacheDir(); if (!fs::CreateAllDirs(path)) { errorstream << "Client: Could not create media cache directory: " << path << std::endl; } } bool IClientMediaDownloader::tryLoadFromCache(const std::string &name, const std::string &sha1, Client *client) { std::ostringstream tmp_os(std::ios_base::binary); bool found_in_cache = m_media_cache.load(hex_encode(sha1), tmp_os); // If found in cache, try to load it from there if (found_in_cache) return checkAndLoad(name, sha1, tmp_os.str(), true, client); return false; } bool IClientMediaDownloader::checkAndLoad( const std::string &name, const std::string &sha1, const std::string &data, bool is_from_cache, Client *client) { const char *cached_or_received = is_from_cache ? "cached" : "received"; const char *cached_or_received_uc = is_from_cache ? "Cached" : "Received";