Showing
72 changed files
with
2008 additions
and
748 deletions
Too many changes to show.
To preserve performance only 72 of 72+ files are displayed.
... | ... | @@ -29,12 +29,19 @@ if __SELFEXE then |
29 | 29 | print("SelfExe gave us app directory of:", dir) |
30 | 30 | fs.mount(dir..fs.getPathSeparator().."game"..fs.getPathSeparator().."thirdparty", "/", true) |
31 | 31 | fs.mount(dir..fs.getPathSeparator().."game", "/", true) |
32 | + fs.setPathAllowed(dir..fs.getPathSeparator().."game", false) | |
32 | 33 | else |
33 | 34 | print("No SelfExe, using basic path") |
34 | 35 | fs.mount("game"..fs.getPathSeparator().."thirdparty", "/", true) |
35 | 36 | fs.mount("game", "/", true) |
36 | 37 | end |
37 | 38 | |
39 | +fs.setPathAllowed(fs.getRealPath("/engines/"), false) | |
40 | +fs.setPathAllowed(fs.getRealPath("/thirdparty/"), false) | |
41 | +fs.setPathAllowed(fs.getRealPath("/addons/"), true) | |
42 | +if fs.getRealPath("/dlcs/") then fs.setPathAllowed(fs.getRealPath("/dlcs/"), true) end | |
43 | +fs.setPathAllowed(fs.getRealPath("/modules/"), true) | |
44 | + | |
38 | 45 | -- Look for a core |
39 | 46 | function get_core(coretype, id) |
40 | 47 | coretype = coretype or "te4core" | ... | ... |
... | ... | @@ -620,7 +620,7 @@ if _OPTIONS.steam then |
620 | 620 | dofile("../steamworks/build/steam-code.lua") |
621 | 621 | end |
622 | 622 | |
623 | -if _OPTIONS.discord then | |
623 | +if _OPTIONS.discord and not _OPTIONS['discord-nolib'] then | |
624 | 624 | project "te4-discord" |
625 | 625 | configuration "linux" |
626 | 626 | kind "SharedLib" | ... | ... |
... | ... | @@ -73,7 +73,7 @@ end |
73 | 73 | -- modules should update this as needed |
74 | 74 | _M.clone_nodes = {player=false, x=false, y=false, |
75 | 75 | fov_computed=false, fov={v={actors={}, actors_dist={}}}, distance_map={v={}}, |
76 | - _mo=false, _last_mo=false, add_mos=false, add_displays=false, | |
76 | + _mo=false, _last_mo=false, add_displays=false, | |
77 | 77 | shader=false, shader_args=false, |
78 | 78 | } |
79 | 79 | --- cloneActor default fields (merged by _M.cloneActor with cloneCustom) |
... | ... | @@ -92,6 +92,13 @@ function _M:cloneActor(post_copy, alt_nodes) |
92 | 92 | if post_copy or self.clone_copy then post_copy = post_copy or {} table.update(post_copy, self.clone_copy or {}, true) end |
93 | 93 | -- Clone all except sub-actors which need to simply reference the same ones |
94 | 94 | local a = self:cloneCustom(alt_nodes, function(d) return not d:isClassName("mod.class.Actor") end, post_copy) |
95 | + -- Handle add_displays as a special case | |
96 | + if self.add_displays then | |
97 | + a.add_displays = {} | |
98 | + for i, d in ipairs(self.add_displays) do | |
99 | + table.insert(a.add_displays, d:cloneFull()) | |
100 | + end | |
101 | + end | |
95 | 102 | a:removeAllMOs() |
96 | 103 | return a, post_copy |
97 | 104 | end | ... | ... |
... | ... | @@ -43,16 +43,17 @@ function _M:init(name, npc, player, data) |
43 | 43 | |
44 | 44 | local f, err = loadfile(self:getChatFile(name)) |
45 | 45 | if not f and err then error(err) end |
46 | - setfenv(f, setmetatable({ | |
46 | + local env = setmetatable({ | |
47 | 47 | cur_chat = self, |
48 | 48 | setDialogWidth = function(w) self.force_dialog_width = w end, |
49 | 49 | newChat = function(c) self:addChat(c) end, |
50 | 50 | setTextFont = function(font, size) self.dialog_text_font = {font, size} end, |
51 | 51 | setAnswerFont = function(font, size) self.dialog_answer_font = {font, size} end, |
52 | - }, {__index=data})) | |
52 | + }, {__index=data}) | |
53 | + setfenv(f, env) | |
53 | 54 | self.default_id = f() |
54 | 55 | |
55 | - self:triggerHook{"Chat:load", data=data} | |
56 | + self:triggerHook{"Chat:load", data=data, env=env} | |
56 | 57 | end |
57 | 58 | |
58 | 59 | --- Get chat file | ... | ... |
... | ... | @@ -32,17 +32,18 @@ frame_oy2 = 15 |
32 | 32 | --- @string text |
33 | 33 | -- @int[opt=60] dur |
34 | 34 | -- @param[opt=colors.Black] color |
35 | -function _M:init(text, dur, color) | |
35 | +function _M:init(text, dur, color, font) | |
36 | 36 | self.text = text |
37 | 37 | self.dur = dur or 60 |
38 | 38 | self.color = color or colors.BLACK |
39 | + self.use_font = font | |
39 | 40 | |
40 | - Base.init(self, {font = {"/data/font/DroidSans-Bold.ttf", 16}}) | |
41 | + Base.init(self, {font = self.use_font or {"/data/font/DroidSans-Bold.ttf", 16}}) | |
41 | 42 | end |
42 | 43 | |
43 | 44 | --- on loaded |
44 | 45 | function _M:loaded() |
45 | - Base.init(self, {font = {"/data/font/DroidSans-Bold.ttf", 16}}) | |
46 | + Base.init(self, {font = self.use_font or {"/data/font/DroidSans-Bold.ttf", 16}}) | |
46 | 47 | end |
47 | 48 | |
48 | 49 | --- Serialization | ... | ... |
... | ... | @@ -475,7 +475,7 @@ function _M:addonMD5(add, base) |
475 | 475 | print("[MODULE LOADER] addon ", add.short_name, " MD5", fmd5, "computed in ", core.game.getTime() - t, vbase) |
476 | 476 | |
477 | 477 | if __module_extra_info.compute_md5_only then |
478 | - local f = io.open(__module_extra_info.compute_md5_only, "a") | |
478 | + local f = fs.open(__module_extra_info.compute_md5_only, "a") | |
479 | 479 | f:write(("%s : addon[%s] md5\n"):format(fmd5, add.version_name)) |
480 | 480 | f:close() |
481 | 481 | end |
... | ... | @@ -982,7 +982,7 @@ function _M:instanciate(mod, name, new_game, no_reboot, extra_module_info) |
982 | 982 | print("[MODULE LOADER] module MD5", module_md5, "computed in ", core.game.getTime() - t) |
983 | 983 | |
984 | 984 | if __module_extra_info.compute_md5_only then |
985 | - local f = io.open(__module_extra_info.compute_md5_only, "w") | |
985 | + local f = fs.open(__module_extra_info.compute_md5_only, "w") | |
986 | 986 | f:write(("%s : module[%s] md5\n"):format(module_md5, mod.version_string)) |
987 | 987 | f:close() |
988 | 988 | end | ... | ... |
... | ... | @@ -205,6 +205,9 @@ function _M:getSubtypeOrder() |
205 | 205 | return self.subtype or "" |
206 | 206 | end |
207 | 207 | |
208 | +--- Describe requirements flags naming | |
209 | +_M.requirement_flags_names = {} | |
210 | + | |
208 | 211 | --- Describe requirements |
209 | 212 | function _M:getRequirementDesc(who) |
210 | 213 | local req = rawget(self, "require") |
... | ... | @@ -212,6 +215,19 @@ function _M:getRequirementDesc(who) |
212 | 215 | |
213 | 216 | local str = tstring{"Requires:", true} |
214 | 217 | |
218 | + if req.flag then | |
219 | + for _, flag in ipairs(req.flag) do | |
220 | + if type(flag) == "table" then | |
221 | + local name = self.requirement_flags_names[flag[1]] or flag[1] | |
222 | + local c = (who:attr(flag[1]) and who:attr(flag[1]) >= flag[2]) and {"color", 0x00,0xff,0x00} or {"color", 0xff,0x00,0x00} | |
223 | + str:add(c, "- ", ("%s (level %d)"):format(name, flag[2]), {"color", "LAST"}, true) | |
224 | + else | |
225 | + local name = self.requirement_flags_names[flag] or flag | |
226 | + local c = who:attr(flag) and {"color", 0x00,0xff,0x00} or {"color", 0xff,0x00,0x00} | |
227 | + str:add(c, "- ", ("%s"):format(name), {"color", "LAST"}, true) | |
228 | + end | |
229 | + end | |
230 | + end | |
215 | 231 | if req.stat then |
216 | 232 | for s, v in pairs(req.stat) do |
217 | 233 | local c = (who:getStat(s) >= v) and {"color", 0x00,0xff,0x00} or {"color", 0xff,0x00,0x00} | ... | ... |
... | ... | @@ -23,6 +23,7 @@ local url = require "socket.url" |
23 | 23 | local ltn12 = require "ltn12" |
24 | 24 | local Dialog = require "engine.ui.Dialog" |
25 | 25 | local UserChat = require "engine.UserChat" |
26 | +local sha1 = require("sha1").sha1 | |
26 | 27 | require "Json2" |
27 | 28 | |
28 | 29 | --- Handles the player profile, possibly online |
... | ... | @@ -98,6 +99,12 @@ function _M:start() |
98 | 99 | self:loadGenericProfile() |
99 | 100 | |
100 | 101 | if self.generic.online and self.generic.online.login and self.generic.online.pass then |
102 | + -- Convert to encrypted pass | |
103 | + if not self.generic.online.v2 then | |
104 | + self.generic.online.pass = sha1(self.generic.online.pass) | |
105 | + self:saveGenericProfile("online", {login=self.generic.online.login, pass=self.generic.online.pass, v2=true}) | |
106 | + end | |
107 | + | |
101 | 108 | self.login = self.generic.online.login |
102 | 109 | self.pass = self.generic.online.pass |
103 | 110 | self:tryAuth() |
... | ... | @@ -130,7 +137,7 @@ function _M:mountProfile(online, module) |
130 | 137 | fs.mkdir(string.format("/profiles/%s/generic/", online and "online" or "offline")) |
131 | 138 | if module then fs.mkdir(string.format("/profiles/%s/modules/%s", online and "online" or "offline", module)) end |
132 | 139 | |
133 | - local path = engine.homepath.."/profiles/"..(online and "online" or "offline") | |
140 | + local path = engine.homepath..fs.getPathSeparator().."profiles"..fs.getPathSeparator()..(online and "online" or "offline") | |
134 | 141 | fs.mount(path, "/current-profile") |
135 | 142 | print("[PROFILE] mounted ", online and "online" or "offline", "on /current-profile") |
136 | 143 | fs.setWritePath(path) |
... | ... | @@ -138,7 +145,7 @@ function _M:mountProfile(online, module) |
138 | 145 | return restore |
139 | 146 | end |
140 | 147 | function _M:umountProfile(online, pop) |
141 | - local path = engine.homepath.."/profiles/"..(online and "online" or "offline") | |
148 | + local path = engine.homepath..fs.getPathSeparator().."profiles"..fs.getPathSeparator()..(online and "online" or "offline") | |
142 | 149 | fs.umount(path) |
143 | 150 | print("[PROFILE] unmounted ", online and "online" or "offline", "from /current-profile") |
144 | 151 | |
... | ... | @@ -147,9 +154,9 @@ end |
147 | 154 | |
148 | 155 | -- Define the fields that are sync'ed online, and how they are sync'ed |
149 | 156 | local generic_profile_defs = { |
150 | - firstrun = {nosync=true, {firstrun="number"}, receive=function(data, save) save.firstrun = data.firstrun end }, | |
151 | - online = {nosync=true, {login="string:40", pass="string:40"}, receive=function(data, save) save.login = data.login save.pass = data.pass end }, | |
152 | - onlinesteam = {nosync=true, {autolog="boolean"}, receive=function(data, save) save.autolog = data.autolog end }, | |
157 | + firstrun = {nosync=true, no_sync=true, {firstrun="number"}, receive=function(data, save) save.firstrun = data.firstrun end }, | |
158 | + online = {nosync=true, no_sync=true, {login="string:40", pass="string:40", v2="number"}, receive=function(data, save) save.login = data.login save.pass = data.pass save.v2 = data.v2 end }, | |
159 | + onlinesteam = {nosync=true, no_sync=true, {autolog="boolean"}, receive=function(data, save) save.autolog = data.autolog end }, | |
153 | 160 | modules_played = { {name="index:string:30"}, {time_played="number"}, receive=function(data, save) max_set(save, data.name, data, "time_played") end, export=function(env) for k, v in pairs(env) do add{name=k, time_played=v} end end }, |
154 | 161 | modules_loaded = { {name="index:string:30"}, {nb="number"}, receive=function(data, save) max_set(save, data.name, data, "nb") end, export=function(env) for k, v in pairs(env) do add{name=k, nb=v} end end }, |
155 | 162 | } |
... | ... | @@ -425,14 +432,16 @@ function _M:checkFirstRun() |
425 | 432 | end |
426 | 433 | |
427 | 434 | function _M:performlogin(login, pass) |
435 | + pass = sha1(pass) | |
436 | + | |
428 | 437 | self.login=login |
429 | 438 | self.pass=pass |
430 | 439 | print("[ONLINE PROFILE] attempting log in ", self.login) |
431 | 440 | self.auth_tried = nil |
432 | 441 | self:tryAuth() |
433 | 442 | self:waitFirstAuth() |
434 | - if (profile.auth) then | |
435 | - self:saveGenericProfile("online", {login=login, pass=pass}) | |
443 | + if profile.auth then | |
444 | + self:saveGenericProfile("online", {login=login, pass=pass, v2=true}) | |
436 | 445 | self:getConfigs("generic") |
437 | 446 | self:syncOnline("generic") |
438 | 447 | end |
... | ... | @@ -477,6 +486,9 @@ function _M:popEvent(specific) |
477 | 486 | end |
478 | 487 | |
479 | 488 | function _M:waitEvent(name, cb, wait_max) |
489 | + -- Dont try as it would fail and we'd fait for nothing | |
490 | + if config.settings.disable_all_connectivity then return end | |
491 | + | |
480 | 492 | -- Wait anwser, this blocks thegame but cant really be avoided :/ |
481 | 493 | local stop = false |
482 | 494 | local first = true |
... | ... | @@ -509,6 +521,9 @@ function _M:noMoreAuthWait() |
509 | 521 | end |
510 | 522 | |
511 | 523 | function _M:waitFirstAuth(timeout) |
524 | + -- Dont try as it would fail and we'd fait for nothing | |
525 | + if config.settings.disable_all_connectivity then return end | |
526 | + | |
512 | 527 | if self.no_more_wait_auth then return end |
513 | 528 | if self.auth_tried and self.auth_tried >= 1 then return end |
514 | 529 | if not self.waiting_auth then return end |
... | ... | @@ -564,7 +579,7 @@ function _M:eventGetNews(e) |
564 | 579 | end |
565 | 580 | |
566 | 581 | function _M:eventIncrLogConsume(e) |
567 | - local module = game.__mod_info.short_name | |
582 | + local module = type(game) == "table" and game.__mod_info.short_name | |
568 | 583 | if not module then return end |
569 | 584 | print("[PROFILE] Server accepted our incr log, deleting") |
570 | 585 | local pop = self:mountProfile(true, module) |
... | ... | @@ -592,6 +607,13 @@ function _M:eventGetConfigs(e) |
592 | 607 | end |
593 | 608 | |
594 | 609 | function _M:eventPushCode(e) |
610 | + if not config.settings.allow_online_events then | |
611 | + if e.return_uuid then | |
612 | + core.profile.pushOrder(string.format("o='CodeReturn' uuid=%q data=%q", e.return_uuid, table.serialize{error='user disabled events, refusing to load code'})) | |
613 | + end | |
614 | + return | |
615 | + end | |
616 | + | |
595 | 617 | local f, err = loadstring(e.code) |
596 | 618 | if not f then |
597 | 619 | if e.return_uuid then |
... | ... | @@ -649,6 +671,9 @@ function _M:getNews(callback, steam) |
649 | 671 | end |
650 | 672 | |
651 | 673 | function _M:tryAuth() |
674 | + -- Dont try as it would fail and we'd fait for nothing | |
675 | + if config.settings.disable_all_connectivity then return end | |
676 | + | |
652 | 677 | print("[ONLINE PROFILE] auth") |
653 | 678 | self.auth_last_error = nil |
654 | 679 | if self.steam_token then |
... | ... | @@ -1019,7 +1044,8 @@ function _M:isDonator(s) |
1019 | 1044 | end |
1020 | 1045 | |
1021 | 1046 | function _M:allowDLC(dlc) |
1022 | - if core.steam then if core.steam.checkDLC(dlc[2]) then return true end end | |
1023 | - if self.auth and self.auth.dlcs and self.auth.dlcs[dlc[1]] then return true end | |
1024 | - return false | |
1047 | + -- if core.steam then if core.steam.checkDLC(dlc[2]) then return true end end | |
1048 | + -- if self.auth and self.auth.dlcs and self.auth.dlcs[dlc[1]] then return true end | |
1049 | + -- return false | |
1050 | + return true | |
1025 | 1051 | end | ... | ... |
... | ... | @@ -253,8 +253,6 @@ Again, thank you, and enjoy Eyal! |
253 | 253 | |
254 | 254 | #{italic}#Your malevolent local god of darkness, #GOLD#DarkGod#{normal}#]]):format(data.donated, data.donated * 10, data.items_vault_slots) |
255 | 255 | Dialog:simpleLongPopup("Thank you!", text, 600) |
256 | - elseif self.uc_ext then | |
257 | - self.uc_ext:event(e) | |
258 | 256 | end |
259 | 257 | elseif e.se == "SelfJoin" then |
260 | 258 | self:addMessage("join", e.channel, profile.auth.login, e.channel, "#{italic}#Joined channel#{normal}#", nil, true) |
... | ... | @@ -329,6 +327,10 @@ Again, thank you, and enjoy Eyal! |
329 | 327 | self.channels_changed = true |
330 | 328 | end |
331 | 329 | |
330 | + if self.uc_ext then | |
331 | + self.uc_ext:event(e) | |
332 | + end | |
333 | + | |
332 | 334 | for fct, _ in pairs(self.on_event) do |
333 | 335 | fct(e) |
334 | 336 | end | ... | ... |
... | ... | @@ -769,7 +769,8 @@ function _M:addEntity(level, e, typ, x, y, no_added) |
769 | 769 | if x and y then level.map(x, y, Map.TRIGGER, e) end |
770 | 770 | end |
771 | 771 | e:check("addedToLevel", level, x, y) |
772 | - e:check("on_added", level, x, y) | |
772 | + e:check("on_added", level, x, y) -- Sustains are activated here | |
773 | + e:check("on_added_final", level, x, y) | |
773 | 774 | end |
774 | 775 | |
775 | 776 | --- If we are loaded we need a new uid | ... | ... |
... | ... | @@ -131,7 +131,7 @@ function _M:onDownload(handlers) |
131 | 131 | local Dialog = require "engine.ui.Dialog" |
132 | 132 | |
133 | 133 | handlers.on_download_request = function(downid, url, file, mime) |
134 | - if mime == "application/t-engine-addon" and self.allow_downloads.addons and url:find("^http://te4%.org/") then | |
134 | + if mime == "application/t-engine-addon" and self.allow_downloads.addons and url:find("^https://te4%.org/") then | |
135 | 135 | local name = file |
136 | 136 | if self._next_download_name and os.time() - self._next_download_name.time <= 3 then name = self._next_download_name.name self._next_download_name = nil end |
137 | 137 | print("Accepting addon download to:", self.dest) |
... | ... | @@ -140,7 +140,7 @@ function _M:onDownload(handlers) |
140 | 140 | game:registerDialog(self.download_dialog) |
141 | 141 | self.view:downloadAction(downid, self.dest) |
142 | 142 | return |
143 | - elseif mime == "application/t-engine-module" and self.allow_downloads.modules and url:find("^http://te4%.org/") then | |
143 | + elseif mime == "application/t-engine-module" and self.allow_downloads.modules and url:find("^https://te4%.org/") then | |
144 | 144 | local name = file |
145 | 145 | if self._next_download_name and os.time() - self._next_download_name.time <= 3 then name = self._next_download_name.name self._next_download_name = nil end |
146 | 146 | print("Accepting module download to:", self.dest) | ... | ... |
... | ... | @@ -49,6 +49,8 @@ end |
49 | 49 | |
50 | 50 | --- place a door in a wall |
51 | 51 | function _M:doorOnWall(wall) |
52 | + print("=WALLLDOOR") | |
53 | + table.print(wall) | |
52 | 54 | if wall.vert then |
53 | 55 | local j = rng.table(table.keys(wall.ps)) |
54 | 56 | self.map(wall.base, j, Map.TERRAIN, self:resolve("door")) |
... | ... | @@ -72,11 +74,11 @@ function _M:addWall(vert, base, p1, p2) |
72 | 74 | ps[z] = nil |
73 | 75 | else |
74 | 76 | rm_map.walled = true |
75 | - if z == p1 or z == p2 or not game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move") or 2 ~= | |
76 | - (game.level.map:checkEntity(x - 1, y, Map.TERRAIN, "block_move") and 1 or 0) + | |
77 | - (game.level.map:checkEntity(x + 1, y, Map.TERRAIN, "block_move") and 1 or 0) + | |
78 | - (game.level.map:checkEntity(x, y - 1, Map.TERRAIN, "block_move") and 1 or 0) + | |
79 | - (game.level.map:checkEntity(x, y + 1, Map.TERRAIN, "block_move") and 1 or 0) then | |
77 | + if z == p1 or z == p2 or not self.map:checkEntity(x, y, Map.TERRAIN, "block_move") or 2 ~= | |
78 | + (self.map:checkEntity(x - 1, y, Map.TERRAIN, "block_move") and 1 or 0) + | |
79 | + (self.map:checkEntity(x + 1, y, Map.TERRAIN, "block_move") and 1 or 0) + | |
80 | + (self.map:checkEntity(x, y - 1, Map.TERRAIN, "block_move") and 1 or 0) + | |
81 | + (self.map:checkEntity(x, y + 1, Map.TERRAIN, "block_move") and 1 or 0) then | |
80 | 82 | ps[z] = nil |
81 | 83 | end |
82 | 84 | end | ... | ... |
... | ... | @@ -24,7 +24,7 @@ require "engine.Generator" |
24 | 24 | --- @classmod engine.generator.map.Hexacle |
25 | 25 | module(..., package.seeall, class.inherit(engine.Generator)) |
26 | 26 | |
27 | -function _M:init(zone, map, grid_list, data) | |
27 | +function _M:init(zone, map, level, data) | |
28 | 28 | engine.Generator.init(self, zone, map, level) |
29 | 29 | data.segment_wide_chance = data.segment_wide_chance or 70 |
30 | 30 | data.nb_segments = data.nb_segments or 8 | ... | ... |
... | ... | @@ -24,7 +24,7 @@ require "engine.Generator" |
24 | 24 | --- @classmod engine.generator.map.Maze |
25 | 25 | module(..., package.seeall, class.inherit(engine.Generator)) |
26 | 26 | |
27 | -function _M:init(zone, map, grid_list, data) | |
27 | +function _M:init(zone, map, level, data) | |
28 | 28 | engine.Generator.init(self, zone, map, level) |
29 | 29 | self.data = data |
30 | 30 | data.widen_w = data.widen_w or 1 | ... | ... |
... | ... | @@ -191,7 +191,7 @@ function _M:tmxLoad(file) |
191 | 191 | local g = self:getLoader(t) |
192 | 192 | local map = lom.parse(data) |
193 | 193 | local mapprops = {} |
194 | - if map:findOne("properties") then mapprops = map:findOne("properties"):findAllAttrs("property", "name", "value") end | |
194 | + if map:findOne("properties", nil, nil, true) then mapprops = map:findOne("properties", nil, nil, true):findAllAttrsValueOrBody("property", "name", "value") end | |
195 | 195 | local w, h = tonumber(map.attr.width), tonumber(map.attr.height) |
196 | 196 | local tw, th = tonumber(map.attr.tilewidth), tonumber(map.attr.tileheight) |
197 | 197 | local chars = {} | ... | ... |
... | ... | @@ -45,10 +45,14 @@ fs.mkdir(fs.getHomePath().."/4.0/") |
45 | 45 | fs.mkdir(fs.getHomePath().."/4.0/profiles/") |
46 | 46 | fs.mkdir(fs.getHomePath().."/4.0/settings/") |
47 | 47 | |
48 | -fs.setPathAllowed(engine.homepath) | |
49 | -fs.setPathAllowed(fs.getRealPath("/addons/")) | |
50 | -if fs.getRealPath("/dlcs/") then fs.setPathAllowed(fs.getRealPath("/dlcs/")) end | |
51 | -fs.setPathAllowed(fs.getRealPath("/modules/")) | |
48 | +fs.setPathAllowed(engine.homepath, true) | |
49 | +fs.setPathAllowed(fs.getRealPath("/addons/"), true) | |
50 | +if fs.getRealPath("/dlcs/") then fs.setPathAllowed(fs.getRealPath("/dlcs/"), true) end | |
51 | +fs.setPathAllowed(fs.getRealPath("/modules/"), true) | |
52 | + | |
53 | +-- Last resort, add currently mounted paths, as readonly, so taht reset mounts work | |
54 | +for _, path in ipairs(fs.getSearchPath()) do fs.setPathAllowed(path) end | |
55 | + | |
52 | 56 | fs.doneSettingPathAllowed() |
53 | 57 | fs.setWritePath(engine.homepath) |
54 | 58 | |
... | ... | @@ -76,14 +80,34 @@ censor_boot = true |
76 | 80 | chat.filter = {} |
77 | 81 | chat.ignores = {} |
78 | 82 | addons = {} |
83 | +allow_online_events = true | |
84 | +disable_all_connectivity = true | |
85 | +upload_charsheet = true | |
79 | 86 | upgrades { v1_0_5=true } |
80 | 87 | ]] |
88 | +local loaded_config_files = {} | |
81 | 89 | for i, file in ipairs(fs.list("/settings/")) do |
82 | 90 | if file:find(".cfg$") then |
83 | 91 | config.load("/settings/"..file) |
92 | + loaded_config_files[file] = true | |
84 | 93 | end |
85 | 94 | end |
86 | 95 | |
96 | +-- Keep the same setting when upgrading | |
97 | +-- What a FRELLING MESS | |
98 | +if not loaded_config_files["disable_all_connectivity.cfg"] and not config.settings.firstrun_gdpr then | |
99 | + config.settings.disable_all_connectivity = false | |
100 | +end | |
101 | + | |
102 | +if config.settings.disable_all_connectivity then | |
103 | + core.game.disableConnectivity() | |
104 | + local function void(t) for _, k in ipairs(table.keys(t)) do t[k] = nil end end | |
105 | + -- if core.steam then void(core.steam) core.steam = nil end | |
106 | + if core.discord then void(core.discord) core.discord = nil end | |
107 | + if core.webview then void(core.webview) core.webview = nil end | |
108 | + if socketcore then void(socketcore) socketcore = nil end | |
109 | +end | |
110 | + | |
87 | 111 | if config.settings.force_safeboot then |
88 | 112 | util.removeForceSafeBoot() |
89 | 113 | core.display.forceSafeMode() | ... | ... |
... | ... | @@ -316,13 +316,15 @@ function _M:aiTalentTargets(t, aitarget, tg, all_targets, ax, ay) |
316 | 316 | friendlyfire = typ.friendlyfire and (type(typ.friendlyfire) == "number" and typ.friendlyfire or 100) or 0 |
317 | 317 | if all_targets then typ.selffire, typ.friendlyfire = 100, 100 end |
318 | 318 | targets = {} |
319 | - self:project(typ, ax, ay, function(px, py) | |
320 | - local tgt = game.level.map(px, py, typ.scan_on or Map.ACTOR) | |
321 | - if tgt and not tgt.dead then | |
322 | - if log_detail > 2 then print("[aiTalentTargets]", t.id, "may affect", px, py, "actor:", tgt.uid, tgt.name) end | |
323 | - targets[#targets+1] = tgt | |
324 | - end | |
325 | - end) | |
319 | + if ax and ay then | |
320 | + self:project(typ, ax, ay, function(px, py) | |
321 | + local tgt = game.level.map(px, py, typ.scan_on or Map.ACTOR) | |
322 | + if tgt and not tgt.dead then | |
323 | + if log_detail > 2 then print("[aiTalentTargets]", t.id, "may affect", px, py, "actor:", tgt.uid, tgt.name) end | |
324 | + targets[#targets+1] = tgt | |
325 | + end | |
326 | + end) | |
327 | + end | |
326 | 328 | end |
327 | 329 | end |
328 | 330 | return targets, selffire, friendlyfire, tg | ... | ... |
... | ... | @@ -51,6 +51,7 @@ function _M:newTalentType(t) |
51 | 51 | assert(t.name, "no talent type name") |
52 | 52 | assert(t.type, "no talent type type") |
53 | 53 | t.description = t.description or "" |
54 | + t.category = t.category or t.type:gsub("/.*", "") | |
54 | 55 | t.points = t.points or 1 |
55 | 56 | t.talents = {} |
56 | 57 | table.insert(self.talents_types_def, t) |
... | ... | @@ -95,7 +96,8 @@ end |
95 | 96 | function _M:init(t) |
96 | 97 | self.talents = t.talents or {} |
97 | 98 | self.talents_types = t.talents_types or {} |
98 | - self.talents_types_mastery = self.talents_types_mastery or {} | |
99 | + self.talents_types_mastery = self.talents_types_mastery or {} | |
100 | + self.talents_mastery_bonus = self.talents_mastery_bonus or {} | |
99 | 101 | self.talents_cd = self.talents_cd or {} |
100 | 102 | self.sustain_talents = self.sustain_talents or {} |
101 | 103 | self.talents_auto = self.talents_auto or {} |
... | ... | @@ -831,7 +833,8 @@ function _M:getTalentLevel(id) |
831 | 833 | else |
832 | 834 | t = _M.talents_def[id] |
833 | 835 | end |
834 | - return t and (self:getTalentLevelRaw(id)) * ((self.talents_types_mastery[t.type[1]] or 0) + 1) or 0 | |
836 | + return t and (self:getTalentLevelRaw(id)) * (self:getTalentMastery(t) or 0) or 0 | |
837 | + | |
835 | 838 | end |
836 | 839 | |
837 | 840 | --- Talent type level, sum of all raw levels of talents inside |
... | ... | @@ -997,12 +1000,13 @@ function _M:getTalentDisplayName(t) |
997 | 1000 | return t.display_name |
998 | 1001 | end |
999 | 1002 | |
1000 | ---- Cooldown all talents by one | |
1003 | +--- Cooldown all talents | |
1001 | 1004 | -- This should be called in your actors "act()" method |
1002 | -function _M:cooldownTalents() | |
1005 | +-- @param turns the number of turns to cooldown the talents | |
1006 | +function _M:cooldownTalents(turns) | |
1003 | 1007 | for tid, c in pairs(self.talents_cd) do |
1004 | 1008 | self.changed = true |
1005 | - self.talents_cd[tid] = self.talents_cd[tid] - 1 | |
1009 | + self.talents_cd[tid] = self.talents_cd[tid] - (turns or 1) | |
1006 | 1010 | if self.talents_cd[tid] <= 0 then |
1007 | 1011 | self.talents_cd[tid] = nil |
1008 | 1012 | if self.onTalentCooledDown then self:onTalentCooledDown(tid) end |
... | ... | @@ -1079,14 +1083,24 @@ function _M:triggerTalent(tid, name, ...) |
1079 | 1083 | |
1080 | 1084 | local t = _M.talents_def[tid] |
1081 | 1085 | name = name or "trigger" |
1082 | - if t[name] then return t[name](self, t, ...) end | |
1086 | + if t[name] then | |
1087 | + self.__talent_running = t | |
1088 | + local ret = {t[name](self, t, ...)} | |
1089 | + self.__talent_running = nil | |
1090 | + return unpack(ret, 1, table.maxn(ret)) | |
1091 | + end | |
1083 | 1092 | end |
1084 | 1093 | |
1085 | 1094 | --- Trigger a talent method |
1086 | 1095 | function _M:callTalent(tid, name, ...) |
1087 | 1096 | local t = _M.talents_def[tid] |
1088 | 1097 | name = name or "trigger" |
1089 | - if t[name] then return t[name](self, t, ...) end | |
1098 | + if t[name] then | |
1099 | + self.__talent_running = t | |
1100 | + local ret = {t[name](self, t, ...)} | |
1101 | + self.__talent_running = nil | |
1102 | + return unpack(ret, 1, table.maxn(ret)) | |
1103 | + end | |
1090 | 1104 | end |
1091 | 1105 | |
1092 | 1106 | --- Trigger all talents matching | ... | ... |
... | ... | @@ -36,6 +36,18 @@ function _M:init() |
36 | 36 | |
37 | 37 | -- Allow scrolling when targetting |
38 | 38 | self.target.on_set_target = function(self, how) |
39 | + if self.target and self.target_type and self.target.x and self.active and self.target_type.stop_before_target and how == "scan" then | |
40 | + local start_x = self.target_type.start_x or self.target_type.x or self.target_type.source_actor and self.target_type.source_actor.x or self.x | |
41 | + local start_y = self.target_type.start_y or self.target_type.y or self.target_type.source_actor and self.target_type.source_actor.y or self.y | |
42 | + local l = core.fov.line(self.target.x, self.target.y, start_x, start_y) | |
43 | + local lx, ly = l:step() | |
44 | + if lx and ly then | |
45 | + self.target.x = lx | |
46 | + self.target.y = ly | |
47 | + self.target.entity = game.level.map(self.target.x, self.target.y, engine.Map.ACTOR) | |
48 | + end | |
49 | + end | |
50 | + | |
39 | 51 | if self.key ~= self.targetmode_key then return end |
40 | 52 | local dx, dy = game.level.map:moveViewSurround(self.target.x, self.target.y, 1, 1, true) |
41 | 53 | if how == "mouse" and (dx ~= 0 or dy ~= 0) then | ... | ... |
... | ... | @@ -29,6 +29,7 @@ allow_late_uuid = false |
29 | 29 | |
30 | 30 | --- Register the character on te4.org and return a UUID for it |
31 | 31 | function _M:getUUID() |
32 | + if game.allowJSONDump and not game:allowJSONDump() then return end | |
32 | 33 | if self.__te4_uuid then return self.__te4_uuid end |
33 | 34 | local uuid = profile:registerNewCharacter(game.__mod_info.short_name) |
34 | 35 | if uuid then |
... | ... | @@ -38,6 +39,7 @@ end |
38 | 39 | |
39 | 40 | --- Call this when a character is saved to upload data to te4.org |
40 | 41 | function _M:saveUUID(do_charball) |
42 | + if game.allowJSONDump and not game:allowJSONDump() then return end | |
41 | 43 | if game:isTainted() then return end |
42 | 44 | if not self.__te4_uuid then |
43 | 45 | -- Try to grab an UUID even after char reg | ... | ... |
... | ... | @@ -222,6 +222,8 @@ A usual problem is shaders and thus should be your first target to disable.]], 7 |
222 | 222 | if reboot_message then |
223 | 223 | Dialog:simpleLongPopup("Message", reboot_message, 700) |
224 | 224 | end |
225 | + | |
226 | + self:checkBootLoginRegister() | |
225 | 227 | end |
226 | 228 | |
227 | 229 | function _M:grabAddons() |
... | ... | @@ -540,39 +542,46 @@ function _M:onQuit() |
540 | 542 | end, "Quit", "Continue") |
541 | 543 | end |
542 | 544 | |
543 | -profile_help_text = [[#LIGHT_GREEN#T-Engine4#LAST# allows you to sync your player profile with the website #LIGHT_BLUE#https://te4.org/#LAST# | |
545 | +profile_help_text = [[Welcome to #LIGHT_GREEN#Tales of Maj'Eyal#LAST#! | |
546 | + | |
547 | +Before you can start dying in many innovative ways we need to ask you about online play. | |
544 | 548 | |
545 | -This allows you to: | |
549 | +This is a #{bold}#single player game#{normal}# but it also features many online features to enhance your gameplay and connect you to the community: | |
546 | 550 | * Play from several computers without having to copy unlocks and achievements. |
547 | -* Keep track of your modules progression, kill count, ... | |
548 | -* Talk ingame to other fellow players | |
549 | -* Cool statistics for each module to help sharpen your gameplay style | |
551 | +* Talk ingame to other fellow players, ask for advice, share your most memorable moments... | |
552 | +* Keep track of your kill count, deaths, most played classes... | |
553 | +* Cool statistics for to help sharpen your gameplay style | |
554 | +* Install official expansions and third-party addons directly from the game, hassle-free | |
555 | +* Access your purchaser / donator bonuses if you have bought the game or donated on https://te4.org/ | |
550 | 556 | * Help the game developers balance and refine the game |
551 | 557 | |
552 | -You will also have a user page on https://te4.org/ where you can show off your achievements to your friends. | |
553 | -This is all optional, you are not forced to use this feature at all, but the developers would thank you if you did as it will make balancing easier. | |
554 | -Online profile requires an internet connection, if not available it will wait and sync when it finds one.]] | |
558 | +You will also have a user page on #LIGHT_BLUE#https://te4.org/#LAST# to show off to your friends. | |
559 | +This is all optional, you are not forced to use this feature at all, but the developer would thank you if you did as it will make balancing easier.]] | |
555 | 560 | |
556 | 561 | function _M:checkFirstTime() |
557 | - if not profile.generic.firstrun and not core.steam then | |
558 | - profile:checkFirstRun() | |
559 | - local text = "Thanks for downloading T-Engine/ToME.\n\n"..profile_help_text | |
560 | - Dialog:yesnocancelLongPopup("Welcome to T-Engine", text, 600, function(ret, cancel) | |
561 | - if cancel then return end | |
562 | - if not ret then | |
563 | - local dialogdef = {} | |
564 | - dialogdef.fct = function(login) self:setPlayerLogin(login) end | |
565 | - dialogdef.name = "login" | |
566 | - dialogdef.justlogin = true | |
567 | - game:registerDialog(require('mod.dialogs.ProfileLogin').new(dialogdef, game.profile_help_text)) | |
568 | - else | |
569 | - local dialogdef = {} | |
570 | - dialogdef.fct = function(login) self:setPlayerLogin(login) end | |
571 | - dialogdef.name = "creation" | |
572 | - dialogdef.justlogin = false | |
573 | - game:registerDialog(require('mod.dialogs.ProfileLogin').new(dialogdef, game.profile_help_text)) | |
562 | + if not profile.generic.firstrun then | |
563 | + local d = require("mod.dialogs.FirstRun").new(profile_help_text) | |
564 | + local mm = self.dialogs[#self.dialogs] | |
565 | + self:unregisterDialog(mm) | |
566 | + self.tooltip = nil | |
567 | + self:registerDialog(d) | |
568 | + end | |
569 | +end | |
570 | + | |
571 | +function _M:checkBootLoginRegister() | |
572 | + if __module_extra_info.boot_and_register then | |
573 | + if core.steam then | |
574 | + local mm = self.dialogs[#self.dialogs] | |
575 | + if mm and mm:isClassName("mod.dialogs.MainMenu") then | |
576 | + mm:loginSteam() | |
574 | 577 | end |
575 | - end, "Register new account", "Log in existing account", "Maybe later") | |
578 | + else | |
579 | + local dialogdef = {} | |
580 | + dialogdef.fct = function(login) self:setPlayerLogin(login) end | |
581 | + dialogdef.name = "creation" | |
582 | + dialogdef.justlogin = false | |
583 | + game:registerDialog(require('mod.dialogs.ProfileLogin').new(dialogdef, game.profile_help_text)) | |
584 | + end | |
576 | 585 | end |
577 | 586 | end |
578 | 587 | ... | ... |

3.31 KB
... | ... | @@ -84,6 +84,7 @@ function _M:init() |
84 | 84 | |
85 | 85 | self.c_list = List.new{width=self.iw, nb_items=#self.list, list=self.list, fct=function(item) end, font={FontPackage:getFont("default")}} |
86 | 86 | |
87 | + self.c_discord = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="discord.png", fct=function() util.browserOpenUrl("https://discord.gg/tales-of-majeyal", {is_external=true}) end} | |
87 | 88 | self.c_facebook = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="facebook.png", fct=function() util.browserOpenUrl("https://www.facebook.com/tales.of.maj.eyal", {is_external=true}) end} |
88 | 89 | self.c_twitter = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="twitter.png", fct=function() util.browserOpenUrl("https://twitter.com/TalesOfMajEyal", {is_external=true}) end} |
89 | 90 | self.c_forums = ButtonImage.new{no_decoration=true, alpha_unfocus=0.5, file="forums.png", fct=function() util.browserOpenUrl("http://forums.te4.org/", {is_external=true}) end} |
... | ... | @@ -92,6 +93,7 @@ function _M:init() |
92 | 93 | {left=0, top=0, ui=self.c_list}, |
93 | 94 | {left=0, bottom=0, absolute=true, ui=self.c_background}, |
94 | 95 | {right=self.c_facebook.w, bottom=0, absolute=true, ui=self.c_version}, |
96 | + {right=0, bottom=self.c_facebook.h+self.c_twitter.h+self.c_forums.h, absolute=true, ui=self.c_discord}, | |
95 | 97 | {right=0, bottom=self.c_facebook.h+self.c_twitter.h, absolute=true, ui=self.c_forums}, |
96 | 98 | {right=0, bottom=self.c_twitter.h, absolute=true, ui=self.c_facebook}, |
97 | 99 | {right=0, bottom=0, absolute=true, ui=self.c_twitter}, |
... | ... | @@ -121,10 +123,12 @@ end |
121 | 123 | function _M:updateUI() |
122 | 124 | local uis = table.clone(self.base_uis) |
123 | 125 | |
124 | - if profile.auth then | |
125 | - self:uiStats(uis) | |
126 | - else | |
127 | - self:uiLogin(uis) | |
126 | + if not config.settings.disable_all_connectivity then | |
127 | + if profile.auth then | |
128 | + self:uiStats(uis) | |
129 | + else | |
130 | + self:uiLogin(uis) | |
131 | + end | |
128 | 132 | end |
129 | 133 | |
130 | 134 | self:loadUI(uis) | ... | ... |
... | ... | @@ -27,7 +27,7 @@ local Textzone = require "engine.ui.Textzone" |
27 | 27 | module(..., package.seeall, class.inherit(Dialog)) |
28 | 28 | |
29 | 29 | function _M:init(dialogdef, profile_help_text) |
30 | - Dialog.init(self, "Online profile "..dialogdef.name, 600, 400) | |
30 | + Dialog.init(self, "Online profile "..dialogdef.name, math.min(800, game.w * 0.9), 400) | |
31 | 31 | self.profile_help_text = profile_help_text |
32 | 32 | self.dialogdef = dialogdef |
33 | 33 | self.alpha = 230 |
... | ... | @@ -65,9 +65,10 @@ function _M:init(dialogdef, profile_help_text) |
65 | 65 | self.c_pass = Textbox.new{title="Password: ", size_title=pwa, text="", chars=30, max_len=20, hide=true, filter=pass_filter, fct=function(text) self:okclick() end} |
66 | 66 | self.c_pass2 = Textbox.new{title=pwa, text="", size_title=pwa, chars=30, max_len=20, hide=true, filter=pass_filter, fct=function(text) self:okclick() end} |
67 | 67 | self.c_email = Textbox.new{title="Email: ", size_title=pwa, text="", chars=30, max_len=60, filter=pass_filter, fct=function(text) self:okclick() end} |
68 | - self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails", default=true, fct=function() self:okclick() end} | |
69 | - self.c_news2 = Textzone.new{text="about important game events from us.", width=self.iw - 20, auto_height=true} | |
68 | + self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails about important game events from us.", default=false, fct=function() self:okclick() end} | |
69 | + self.c_age = Checkbox.new{title="You at least 16 years old, or have parental authorization to play the game.", default=false, fct=function() self:okclick() end} | |
70 | 70 | local ok = require("engine.ui.Button").new{text="Create", fct=function() self:okclick() end} |
71 | + local privacy = require("engine.ui.Button").new{text="Privacy Policy (opens in browser)", fct=function() self:privacypolicy() end} | |
71 | 72 | local cancel = require("engine.ui.Button").new{text="Cancel", fct=function() self:cancelclick() end} |
72 | 73 | |
73 | 74 | self:loadUI{ |
... | ... | @@ -77,9 +78,10 @@ function _M:init(dialogdef, profile_help_text) |
77 | 78 | {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+5, ui=self.c_pass2}, |
78 | 79 | {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+10, ui=self.c_email}, |
79 | 80 | {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+self.c_email.h+10, ui=self.c_news}, |
80 | - {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+self.c_email.h+self.c_news2.h+10, ui=self.c_news2}, | |
81 | + {left=0, top=self.c_desc.h+self.c_login.h+self.c_pass.h+self.c_pass2.h+self.c_email.h+self.c_news.h+10, ui=self.c_age}, | |
81 | 82 | {left=0, bottom=0, ui=ok}, |
82 | 83 | {right=0, bottom=0, ui=cancel}, |
84 | + {hcenter=0, bottom=0, ui=privacy}, | |
83 | 85 | } |
84 | 86 | self:setFocus(self.c_login) |
85 | 87 | end |
... | ... | @@ -108,6 +110,10 @@ function _M:okclick() |
108 | 110 | self:simplePopup("Email", "Your email seems invalid") |
109 | 111 | return |
110 | 112 | end |
113 | + if not self.c_age.checked then | |
114 | + self:simplePopup("Age Check", "You need to be 16 years old or more or to have parental authorization to play this game.") | |
115 | + return | |
116 | + end | |
111 | 117 | |
112 | 118 | game:unregisterDialog(self) |
113 | 119 | game:createProfile({create=self.c_email and true or false, login=self.c_login.text, pass=self.c_pass.text, email=self.c_email and self.c_email.text, news=self.c_news and self.c_news.checked}) |
... | ... | @@ -116,3 +122,7 @@ end |
116 | 122 | function _M:cancelclick() |
117 | 123 | self.key:triggerVirtual("EXIT") |
118 | 124 | end |
125 | + | |
126 | +function _M:privacypolicy() | |
127 | + util.browserOpenUrl("https://te4.org/privacy-policy-data", {is_external=true}) | |
128 | +end | ... | ... |
... | ... | @@ -27,7 +27,7 @@ local Textzone = require "engine.ui.Textzone" |
27 | 27 | module(..., package.seeall, class.inherit(Dialog)) |
28 | 28 | |
29 | 29 | function _M:init() |
30 | - Dialog.init(self, "Steam User Account", 500, 400) | |
30 | + Dialog.init(self, "Steam User Account", math.min(800, game.w * 0.9), 400) | |
31 | 31 | self.alpha = 230 |
32 | 32 | |
33 | 33 | self.c_desc = Textzone.new{width=math.floor(self.iw - 10), auto_height=true, text=[[Welcome to #GOLD#Tales of Maj'Eyal#LAST#. |
... | ... | @@ -43,18 +43,20 @@ Luckily this is very easy to do: you only require a profile name and optionally |
43 | 43 | |
44 | 44 | self.c_login = Textbox.new{title="Username: ", text="", chars=30, max_len=20, fct=function(text) self:okclick() end} |
45 | 45 | self.c_email = Textbox.new{title="Email: ", size_title=self.c_login.title, text="", chars=30, max_len=60, fct=function(text) self:okclick() end} |
46 | - self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails", default=true, fct=function() self:okclick() end} | |
47 | - self.c_news2 = Textzone.new{text="about important game events from us.", width=self.iw - 20, auto_height=true} | |
46 | + self.c_news = Checkbox.new{title="Accept to receive #{bold}#very infrequent#{normal}# (a few per year) mails about important game events from us.", default=false, fct=function() self:okclick() end} | |
47 | + self.c_age = Checkbox.new{title="You at least 16 years old, or have parental authorization to play the game.", default=false, fct=function() self:okclick() end} | |
48 | 48 | local ok = require("engine.ui.Button").new{text="Register", fct=function() self:okclick() end} |
49 | 49 | local cancel = require("engine.ui.Button").new{text="Cancel", fct=function() self:cancelclick() end} |
50 | + local privacy = require("engine.ui.Button").new{text="Privacy Policy (opens in browser)", fct=function() self:privacypolicy() end} | |
50 | 51 | self:loadUI{ |
51 | 52 | {left=0, top=0, ui=self.c_desc}, |
52 | 53 | {left=0, top=self.c_desc.h, ui=self.c_login}, |
53 | 54 | {left=0, top=self.c_desc.h+self.c_login.h+5, ui=self.c_email}, |
54 | 55 | {left=0, top=self.c_desc.h+self.c_login.h+self.c_email.h+5, ui=self.c_news}, |
55 | - {left=0, top=self.c_desc.h+self.c_login.h+self.c_email.h+self.c_news.h+5, ui=self.c_news2}, | |
56 | + {left=0, top=self.c_desc.h+self.c_login.h+self.c_email.h+self.c_news.h+5, ui=self.c_age}, | |
56 | 57 | {left=0, bottom=0, ui=ok}, |
57 | 58 | {right=0, bottom=0, ui=cancel}, |
59 | + {hcenter=0, bottom=0, ui=privacy}, | |
58 | 60 | } |
59 | 61 | self:setFocus(self.c_login) |
60 | 62 | self:setupUI(true, true) |
... | ... | @@ -74,7 +76,11 @@ function _M:okclick() |
74 | 76 | self:simplePopup("Email", "Your email does not look right.") |
75 | 77 | return |
76 | 78 | end |
77 | - | |
79 | + if not self.c_age.checked then | |
80 | + self:simplePopup("Age Check", "You need to be 16 years old or more or to have parental authorization to play this game.") | |
81 | + return | |
82 | + end | |
83 | + | |
78 | 84 | local d = self:simpleWaiter("Registering...", "Registering on https://te4.org/, please wait...") core.display.forceRedraw() |
79 | 85 | d:timeout(30, function() Dialog:simplePopup("Steam", "Steam client not found.") end) |
80 | 86 | core.steam.sessionTicket(function(ticket) |
... | ... | @@ -99,3 +105,7 @@ end |
99 | 105 | function _M:cancelclick() |
100 | 106 | self.key:triggerVirtual("EXIT") |
101 | 107 | end |
108 | + | |
109 | +function _M:privacypolicy() | |
110 | + util.browserOpenUrl("https://te4.org/privacy-policy-data", {is_external=true}) | |
111 | +end | ... | ... |
... | ... | @@ -129,6 +129,9 @@ _M.temporary_values_conf.force_melee_damtype = "last" |
129 | 129 | -- AI |
130 | 130 | _M.temporary_values_conf.ai_move = "last" |
131 | 131 | |
132 | +-- Misc | |
133 | +_M.temporary_values_conf.death_dialog = "last" | |
134 | + | |
132 | 135 | _M.projectile_class = "mod.class.Projectile" |
133 | 136 | |
134 | 137 | function _M:init(t, no_default) |
... | ... | @@ -600,17 +603,53 @@ function _M:actBase() |
600 | 603 | end |
601 | 604 | |
602 | 605 | -- Cooldown talents after effects, because some of them involve breaking sustains. |
603 | - if not self:attr("no_talents_cooldown") then self:cooldownTalents() end | |
606 | + | |
607 | + if not self:attr("no_talents_cooldown") then | |
608 | + if self:attr("half_talents_cooldown") then self:cooldownTalents(0.5) else self:cooldownTalents(1) end | |
609 | + end | |
604 | 610 | |
605 | 611 | self:checkStillInCombat() |
606 | 612 | end |
607 | 613 | |
614 | +function _M:hasProc(proc) | |
615 | + if not self.turn_procs then return end | |
616 | + if self.turn_procs[proc] then | |
617 | + return self.turn_procs[proc] | |
618 | + elseif self.turn_procs.multi then | |
619 | + return self.turn_procs.multi[proc] | |
620 | + end | |
621 | +end | |
622 | + | |
623 | +function _M:setProc(name, val, turns) | |
624 | + turns = turns or 1 | |
625 | + val = val or true | |
626 | + local proc = {val = val, turns = turns} | |
627 | + if turns > 1 then | |
628 | + table.set(self, "turn_procs", "multi", name, proc) | |
629 | + else | |
630 | + self.turn_procs[name] = proc | |
631 | + end | |
632 | +end | |
633 | + | |
608 | 634 | -- General entry point for Actors to act, called by NPC:act or Player:act |
609 | 635 | function _M:act() |
610 | 636 | if not engine.Actor.act(self) then return end |
611 | 637 | |
612 | 638 | self.changed = true |
639 | + | |
640 | + -- Store procs with more than 1 turn remaining and re-add them after we clear turn_procs | |
641 | + local temp = {} | |
642 | + if self.turn_procs.multi then | |
643 | + for proc, val in pairs(self.turn_procs.multi) do | |
644 | + if proc then | |
645 | + self.turn_procs.multi[proc].turns = self.turn_procs.multi[proc].turns - 1 | |
646 | + if self.turn_procs.multi[proc].turns > 0 then temp[proc] = val end | |
647 | + end | |
648 | + end | |
649 | + end | |
650 | + | |
613 | 651 | self.turn_procs = {} |
652 | + if temp then self.turn_procs.multi = temp end | |
614 | 653 | |
615 | 654 | -- Break some sustains if certain resources are too low |
616 | 655 | -- Note: force_talent_ignore_ressources has no effect here |
... | ... | @@ -1312,7 +1351,7 @@ function _M:move(x, y, force) |
1312 | 1351 | |
1313 | 1352 | -- Confused ? |
1314 | 1353 | if not force and self:attr("confused") then |
1315 | - if rng.percent(self:attr("confused")) then | |
1354 | + if rng.percent(util.bound(self:attr("confused"), 0, 50)) then | |
1316 | 1355 | x, y = self.x + rng.range(-1, 1), self.y + rng.range(-1, 1) |
1317 | 1356 | end |
1318 | 1357 | end |
... | ... | @@ -1406,7 +1445,7 @@ function _M:move(x, y, force) |
1406 | 1445 | end |
1407 | 1446 | |
1408 | 1447 | -- Break channels |
1409 | - if moved then | |
1448 | + if not force and moved and ox and oy and (ox ~= self.x or oy ~= self.y) then | |
1410 | 1449 | self:breakPsionicChannel() |
1411 | 1450 | self:breakSpacetimeTuning() |
1412 | 1451 | end |
... | ... | @@ -1593,7 +1632,7 @@ function _M:teleportRandom(x, y, dist, min_dist) |
1593 | 1632 | |
1594 | 1633 | -- after moving |
1595 | 1634 | if self:attr("defense_on_teleport") or self:attr("resist_all_on_teleport") or self:attr("effect_reduction_on_teleport") then |
1596 | - self:setEffect(self.EFF_OUT_OF_PHASE, 5, {defense=self:attr("defense_on_teleport") or 0, resists=self:attr("resist_all_on_teleport") or 0, effect_reduction=self:attr("effect_reduction_on_teleport") or 0}) | |
1635 | + self:setEffect(self.EFF_OUT_OF_PHASE, 5, {}) | |
1597 | 1636 | end |
1598 | 1637 | end |
1599 | 1638 | |
... | ... | @@ -1860,7 +1899,8 @@ function _M:tooltip(x, y, seen_by) |
1860 | 1899 | if self.type == "humanoid" or self.type == "giant" then ts:add({"font","italic"}, "(", self.female and "female" or "male", ")", {"font","normal"}, true) else ts:add(true) end |
1861 | 1900 | ts:add(self.type:capitalize(), " / ", self.subtype:capitalize(), true) |
1862 | 1901 | ts:add("Rank: ") ts:merge(rank_color:toTString()) ts:add(rank, {"color", "WHITE"}, true) |
1863 | - ts:add({"color", 0, 255, 255}, ("Level: %d"):format(self.level), {"color", "WHITE"}, true) | |
1902 | + if self.hide_level_tooltip then ts:add({"color", 0, 255, 255}, "Level: unknown", {"color", "WHITE"}, true) | |
1903 | + else ts:add({"color", 0, 255, 255}, ("Level: %d"):format(self.level), {"color", "WHITE"}, true) end | |
1864 | 1904 | if self:attr("invulnerable") then ts:add({"color", "PURPLE"}, "INVULNERABLE!", true) end |
1865 | 1905 | ts:add({"color", 255, 0, 0}, ("HP: %d (%d%%)"):format(self.life, self.life * 100 / self.max_life), {"color", "WHITE"}) |
1866 | 1906 | |
... | ... | @@ -2018,7 +2058,11 @@ function _M:tooltip(x, y, seen_by) |
2018 | 2058 | if e.type == "physical" then |
2019 | 2059 | effphysical:add(true, "- ", {"color", "LIGHT_RED"}, desceffect(e, p, dur), {"color", "WHITE"} ) |
2020 | 2060 | elseif e.type == "magical" then |
2021 | - effmagical:add(true, "- ", {"color", "DARK_ORCHID"}, desceffect(e, p, dur), {"color", "WHITE"} ) | |
2061 | + if e.subtype and e.subtype.disease then | |
2062 | + effmagical:add(true, "- ", {"color", "DARK_GREEN"}, desceffect(e, p, dur), {"color", "WHITE"} ) | |
2063 | + else | |
2064 | + effmagical:add(true, "- ", {"color", "DARK_ORCHID"}, desceffect(e, p, dur), {"color", "WHITE"} ) | |
2065 | + end | |
2022 | 2066 | elseif e.type == "mental" then |
2023 | 2067 | effmental:add(true, "- ", {"color", "YELLOW"}, desceffect(e, p, dur), {"color", "WHITE"} ) |
2024 | 2068 | elseif e.type == "other" then |
... | ... | @@ -2386,14 +2430,6 @@ function _M:onTakeHit(value, src, death_note) |
2386 | 2430 | end |
2387 | 2431 | end |
2388 | 2432 | |
2389 | - if value > 0 and self:isTalentActive(self.T_BONE_SHIELD) then | |
2390 | - local t = self:getTalentFromId(self.T_BONE_SHIELD) | |
2391 | - if t.absorb(self, t, self:isTalentActive(self.T_BONE_SHIELD)) then | |
2392 | - game:delayedLogDamage(src, self, 0, ("#SLATE#(%d to bones)#LAST#"):format(value), false) | |
2393 | - value = 0 | |
2394 | - end | |
2395 | - end | |
2396 | - | |
2397 | 2433 | if value <=0 then return 0 end |
2398 | 2434 | if self.knowTalent and (self:knowTalent(self.T_SEETHE) or self:knowTalent(self.T_GRIM_RESOLVE)) then |
2399 | 2435 | if not self:hasEffect(self.EFF_CURSED_FORM) then |
... | ... | @@ -2583,7 +2619,7 @@ function _M:onTakeHit(value, src, death_note) |
2583 | 2619 | end |
2584 | 2620 | |
2585 | 2621 | -- Bloodlust! |
2586 | - if value > 0 and src and src.knowTalent and src:knowTalent(src.T_BLOODLUST) then | |
2622 | + if value > 0 and src and not (src == self) and src.knowTalent and src:knowTalent(src.T_BLOODLUST) then | |
2587 | 2623 | src:setEffect(src.EFF_BLOODLUST, 1, {}) |
2588 | 2624 | end |
2589 | 2625 | |
... | ... | @@ -2722,7 +2758,7 @@ function _M:onTakeHit(value, src, death_note) |
2722 | 2758 | local vt = self:getTalentFromId(self.T_LEECH) |
2723 | 2759 | self:incVim(vt.getVim(self, vt)) |
2724 | 2760 | self:heal(vt.getHeal(self, vt), src) |
2725 | - if self.player then src:logCombat(src, "#AQUAMARINE#You leech a part of #Target#'s vim.") end | |
2761 | + --if self.player then src:logCombat(src, "#AQUAMARINE#You leech a part of #Target#'s vim.") end | |
2726 | 2762 | end |
2727 | 2763 | |
2728 | 2764 | -- Invisible on hit |
... | ... | @@ -2860,7 +2896,7 @@ end |
2860 | 2896 | |
2861 | 2897 | function _M:takeHit(value, src, death_note) |
2862 | 2898 | self:enterCombatStatus(src) |
2863 | - if src.enterCombatStatus then src:enterCombatStatus(self) end | |
2899 | + if src and src.enterCombatStatus then src:enterCombatStatus(self) end | |
2864 | 2900 | |
2865 | 2901 | for eid, p in pairs(self.tmp) do |
2866 | 2902 | local e = self.tempeffect_def[eid] |
... | ... | @@ -2879,15 +2915,6 @@ function _M:takeHit(value, src, death_note) |
2879 | 2915 | |
2880 | 2916 | if src and src.fireTalentCheck then src:fireTalentCheck("callbackOnDealDamage", val, self, dead, death_note) end |
2881 | 2917 | |
2882 | - if dead and src and src.attr and src:attr("overkill") and src.project and not src.turn_procs.overkill then | |
2883 | - src.turn_procs.overkill = true | |
2884 | - local dam = (self.die_at - self.life) * src:attr("overkill") / 100 | |
2885 | - local incdam = self.inc_damage | |
2886 | - self.inc_damage = {} | |
2887 | - src:project({type="ball", radius=2, selffire=false, x=self.x, y=self.y}, self.x, self.y, DamageType.BLIGHT, dam, {type="acid"}) | |
2888 | - self.inc_damage = incdam | |
2889 | - end | |
2890 | - | |
2891 | 2918 | return dead, val |
2892 | 2919 | end |
2893 | 2920 | |
... | ... | @@ -3292,25 +3319,391 @@ function _M:die(src, death_note) |
3292 | 3319 | return true |
3293 | 3320 | end |
3294 | 3321 | |
3295 | -function _M:learnStats(statorder) | |
3296 | - self.auto_stat_cnt = self.auto_stat_cnt or 1 | |
3322 | +--- Learn stats (stat increment) in a specified order up to a maximum | |
3323 | +-- @param[table] statorder: an orded list of stat ids in which to learn, generally all learned in one character level | |
3324 | +-- @param repeats: maximum number of times to apply the statorder <1> | |
3325 | +function _M:learnStats(statorder, repeats) | |
3326 | + statorder.idx = statorder.idx or 1 | |
3327 | + repeats = (repeats or 1)*#statorder | |
3297 | 3328 | local nb = 0 |
3298 | 3329 | local max = 60 |
3299 | 3330 | |
3300 | - -- Allow to go over a natural 60, up to 80 at level 50 | |
3331 | + -- Allow stats to go over a natural 60, up to 80 at level 50 | |
3301 | 3332 | if not self.no_auto_high_stats then max = 60 + (self.level * 20 / 50) end |
3302 | 3333 | |
3303 | - while self.unused_stats > 0 do | |
3304 | - if self:getStat(statorder[self.auto_stat_cnt]) < max then | |
3305 | - self:incIncStat(statorder[self.auto_stat_cnt], 1) | |
3334 | + while self.unused_stats > 0 and nb < repeats do | |
3335 | + if self:getStat(statorder[statorder.idx]) < max then | |
3336 | + self:incIncStat(statorder[statorder.idx], 1) | |
3306 | 3337 | self.unused_stats = self.unused_stats - 1 |
3307 | 3338 | end |
3308 | - self.auto_stat_cnt = util.boundWrap(self.auto_stat_cnt + 1, 1, #statorder) | |
3339 | + statorder.idx = util.boundWrap(statorder.idx + 1, 1, #statorder) | |
3309 | 3340 | nb = nb + 1 |
3310 | - if nb >= #statorder then break end | |
3311 | 3341 | end |
3312 | 3342 | end |
3313 | 3343 | |
3344 | +--- Actor learns/advances in a character class, randomly gaining stats and learning talents based on levels in the class | |
3345 | +-- When first called, the actor gains starting stat bonuses and talents and learns talent categories according to the birth class descriptor | |
3346 | +-- Note: this does not handle any special class requirements, such as creating a golem, generating equipment, etc. | |
3347 | +-- @see game.state:applyRandomClass | |
3348 | +-- @param c_data[table] contains info on the class to learn/levelup in: | |
3349 | +-- @field class: name (birth descriptor subclass name) of the character class to learn ("Bulwark", "Solipsist", ...) | |
3350 | +-- @field start_level: starting actor level to begin leveling in the class <1> | |
3351 | +-- @field level_rate: rate levels in character class are gained as % of actor level <100> | |
3352 | +-- @field check_talents_level: set true to enforce character level limits on talent levels (i.e. level 5 at level 50) <nil> | |
3353 | +-- @field level_by_class: set true to use class level rather than actor level when checking talent/stat limits <nil> | |
3354 | +-- @foe;d ignore_special: set true to skip checking talent special requirements | |
3355 | +-- @field auto_sustain: set true to automatically turn on sustained talents learned <nil> | |
3356 | +-- @field tt_focus: talent type focus, higher values cause talent selections to be focused within fewer talent types <3> | |
3357 | +-- @field use_actor_points: set true to apply stat/talent points from base actor levels in addtion to class levels <nil> | |
3358 | +-- The following fields are automatically generated or updated from the class birth descriptor: | |
3359 | +-- @field ttypes[table]: talent trees to learn talents from (updated with birth descriptor) | |
3360 | +-- {talent_type_name = {[1]=known, [2]=mastery_add}, ...} | |
3361 | +-- @field auto_stats: ordered list of stat ids to use when applying unused_stats points | |
3362 | +-- generated from class descriptor by default, set false to disable, see Actor:learnStats | |
3363 | +-- Additional talent inputs for each talent definition t: | |
3364 | +-- t.random_boss_rarity: if defined, the percent chance the talent may be learned each time randomly selected | |
3365 | +function _M:levelupClass(c_data) | |
3366 | + c_data.last_level = c_data.last_level or 0 | |
3367 | + c_data.start_level = c_data.start_level or 1 | |
3368 | + | |
3369 | + local new_level = math.ceil((self.level - c_data.start_level + 1)*(c_data.level_rate or 100)/100) | |
3370 | + | |
3371 | + if new_level <= c_data.last_level then return end | |
3372 | + print("[Actor:levelupClass]", self.name, "auto level up", c_data.class, c_data.last_level, "-->", new_level, c_data) | |
3373 | + | |
3374 | + local ttypes | |
3375 | + -- temporarily remove any previous stat/talent points if they won't be used | |
3376 | + local base_points = {self.unused_stats, self.unused_talents, self.unused_generics} | |
3377 | + if not c_data.use_actor_points then | |
3378 | + self.unused_stats, self.unused_talents, self.unused_generics = 0, 0, 0 | |
3379 | + end | |
3380 | + | |
3381 | + -- Initialize if needed, updating auto_classes table | |
3382 | + if c_data.class and not c_data.initialized then -- build talent category list from the class | |
3383 | + local Birther = require "engine.Birther" | |
3384 | + local c_def = Birther.birth_descriptor_def.subclass[c_data.class] | |
3385 | + if not c_def then | |
3386 | + print("[Actor:levelupClass] ### undefined class:", c_data.class) | |
3387 | + return | |
3388 | + end | |
3389 | + | |
3390 | + local mclasses = Birther.birth_descriptor_def.class | |
3391 | + local mclass = nil | |
3392 | + for name, data in pairs(mclasses) do | |
3393 | + if data.descriptor_choices and data.descriptor_choices.subclass and data.descriptor_choices.subclass[c_def.name] then mclass = data break end | |
3394 | + end | |
3395 | + if not mclass then | |
3396 | + print("[Actor:levelupClass] ###class", c_data.class, "has no parent class type###") | |
3397 | + return | |
3398 | + end | |
3399 | + | |
3400 | + print(("[Actor:levelupClass] %s %s ## Initialzing auto_class %s (%s) %s%% level_rate from level %s ##"):format(self.uid, self.name, c_data.class, mclass.name, c_data.level_rate, c_data.start_level)) | |
3401 | + | |
3402 | + -- update class descriptor list and build inherent power sources | |
3403 | + self.descriptor = self.descriptor or {} | |
3404 | + self.descriptor.classes = self.descriptor.classes or {} | |
3405 | + table.append(self.descriptor.classes, {c_def.name}) | |
3406 | + | |
3407 | + -- build inherent power sources and forbidden power sources | |
3408 | + -- self.forbid_power_source --> self.not_power_source used for classes | |
3409 | + self.power_source = table.merge(self.power_source or {}, c_def.power_source or {}) | |
3410 | + self.not_power_source = table.merge(self.not_power_source or {}, c_def.not_power_source or {}) | |
3411 | + -- update power source parameters with the new class | |
3412 | + self.not_power_source, self.power_source = game.state:updatePowers(game.state:attrPowers(self, self.not_power_source), self.power_source) | |
3413 | + print(" *** power types: not_power_source =", table.concat(table.keys(self.not_power_source),","), "power_source =", table.concat(table.keys(self.power_source),",")) | |
3414 | + | |
3415 | + -- apply class stat bonuses and set up class auto_stats (in auto_classes) | |
3416 | + local auto_stats | |
3417 | + if c_def.stats and c_data.auto_stats ~= false then | |
3418 | + auto_stats = {} | |
3419 | + for stat, v in pairs(c_def.stats or {}) do | |
3420 | + local stat_id = self.stats_def[stat].id | |
3421 | + self.stats[stat_id] = (self.stats[stat_id] or 10) + v | |
3422 | + for i = 1, v do auto_stats[#auto_stats+1] = stat_id end | |
3423 | + end | |
3424 | + c_data.auto_stats = auto_stats | |
3425 | + end | |
3426 | + c_data.auto_stats = c_data.auto_stats or auto_stats | |
3427 | + | |
3428 | + ttypes = {} | |
3429 | + for tt, d in pairs(mclass.talents_types or {}) do | |
3430 | + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2]) | |
3431 | + ttypes[tt] = table.clone(d) | |
3432 | + end | |
3433 | + for tt, d in pairs(mclass.unlockable_talents_types or {}) do | |
3434 | + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2]) | |
3435 | + ttypes[tt] = table.clone(d) | |
3436 | + end | |
3437 | + for tt, d in pairs(c_def.talents_types or {}) do | |
3438 | + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2]) | |
3439 | + ttypes[tt] = table.clone(d) | |
3440 | + end | |
3441 | + for tt, d in pairs(c_def.unlockable_talents_types or {}) do | |
3442 | + self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2]) | |
3443 | + ttypes[tt] = table.clone(d) | |
3444 | + end | |
3445 | + | |
3446 | + -- set up input talent categories specified (generally for non-class talents) | |
3447 | + if c_data.ttypes then | |
3448 | + for tt, d in pairs(c_data.ttypes) do | |
3449 | + if not self:knowTalentType(tt) then | |
3450 | + if type(d) ~= "table" then | |
3451 | + d={true, type(d) == "number" and d or rng.range(1, 3)*0.1} | |
3452 | + else d=table.clone(d) | |
3453 | + end | |
3454 | + self:learnTalentType(tt, d[1]) | |
3455 | + self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2]) | |
3456 | + ttypes[tt] = table.mergeAdd(ttypes[tt] or {}, d) | |
3457 | + end | |
3458 | + end | |
3459 | + end | |
3460 | + if not next(ttypes) then -- if not specified, use all known talent types | |
3461 | + for tt, known in pairs(self.talents_types) do | |
3462 | + ttypes[tt] = {known, (self:getTalentTypeMastery(tt) or 1) - 1} | |
3463 | + end | |
3464 | + end | |
3465 | + | |
3466 | + -- Note: could limit number of talent trees selected here to limit # talents learned | |
3467 | + | |
3468 | +--print("\t *** auto_levelup initialized talent category choices:", mclass.name , c_def.name , "\n\t") | |
3469 | + -- set up (semi-random) rarity levels for talent categories | |
3470 | + -- This tends to focus learned talents within certain trees (usually those with improved mastery) | |
3471 | + local tt_count = 0 | |
3472 | + local unknown_tt={} | |
3473 | + local tt_focus = c_data.tt_focus or 3 | |
3474 | + for tt, d in pairs(ttypes) do | |
3475 | + d.tt = tt | |
3476 | + tt_count = tt_count + 1 | |
3477 | + d.tt_count = tt_count | |
3478 | + d.rarity = d.rarity or (1.3 + 0.5*tt_count)/math.max(0.1, self:getTalentTypeMastery(tt)*rng.float(0.1, tt_focus))^2 | |
3479 | +--print(("\t *** %-40s rarity %5.3f"):format(tt, d.rarity)) | |
3480 | + if not d[1] then table.insert(unknown_tt, d) end | |
3481 | + end | |
3482 | + c_data.unknown_tt = unknown_tt | |