localized all the variables in the .lua file
[minetest-xpmod:xp.git] / init.lua
1 -- Copyright 2012 Andrew Engelbrecht.
2 -- This file is licensed under the WTFPL.
3
4 ----------------------------------
5
6 -- Some variables you can change:
7 ----------------------------------
8
9 -- How often (in seconds) xplevels file saves
10 local save_delta = 10
11 -- Max level a player can become; set to 0 for no level cap
12 local max_level = 32
13
14 ----------------------------------
15
16 -- some mod variables:
17 ----------------------------------
18
19 local xpmodver = "0.0.1"
20 local ffver = 1
21 local ffversupport = {1}
22 local file_name = "xplevels"
23
24 local xplevels_file = minetest.get_worldpath() .. "/" .. file_name
25 local xplevels = {}
26 local playert = {}
27 -- dynamically updated to note changes since last save to file:
28 local changed = false
29 -- automatically populated with list of xp needed to gain next level
30 local lptable = nil
31
32 ----------------------------------
33
34 -- function definitions:
35 ----------------------------------
36
37 -- prints out some default error text, the text passed as a parameter.
38 -- then the function kills the server.
39 local function parseerr(text)
40     print("")
41     print("*** xp module error:")
42     print("*** error parsing /"..file_name.." file!")
43     print("***")
44     print("*** "..text)
45     print("***")
46     print("")
47     os.exit()
48 end
49
50 -- extracts xp/level info per skill type for each user
51 -- pass it a line from the xplevels_file.
52 -- it returns the user name and a table of their stats
53 local function parselinev1(line)
54     local s,e,ign,spaceonly,laste,name,statstr,stkind,stlevel,stxp,badskl,test
55     local stats = {}
56
57     -- grabs any space or or an empty string on a blank line
58     ign,ign,spaceonly = string.find(line, "^(%s*)$")
59     if spaceonly ~= nil then
60         -- this is an empty line
61         return nil, nil
62     end
63
64     -- grabs text before the space
65     s,e,name = string.find(line, "^([^%s]+)%s")
66     if name == nil then
67         parseerr("missing space after user name:\n"..line)
68     end
69
70     laste = e
71     repeat
72         -- grabs the next set of text in parenthesis:
73         s,e,statstr = string.find(line, "^%s-%((.-)%)", e + 1)
74
75         if statstr ~= nil then
76             laste = e
77             -- grabs all the data in the parenthesis:
78             ign,ign,stkind,stlevel,stxp = string.find(statstr, "^([^%s]+): level: (%d+) xp: (%d+)$")
79
80             if stxp == nil then
81                 parseerr("bad text between parenthesis:\n"..statstr.."\n*** in this bad line:\n"..line)
82             end
83
84             stats[stkind] = {xp = tonumber(stxp), level = tonumber(stlevel)}
85         end
86     until statstr == nil
87
88     -- there should only be white space at the end of the line:
89     ign,ign,test = string.find(line, "^%s*(.-)$", laste + 1)
90     if test ~= "" then
91         -- finds the first string of text without parenthesis
92         ign,ign,badskl = string.find(line, "^%s*([^%s]+).*$", laste + 1)
93         parseerr("skill\n"..badskl.."\n*** lacks parenthesis in this line:\n"..line)
94     end
95
96     return name, stats
97 end
98
99 -- checks the version and format version string.
100 -- these signify the version of the xp mod that made the file,
101 -- and the formating style that was used.
102 -- pass it the first line of the xplevels_file
103 -- returns a pointer to the appropriate parser function
104 local function checkfver(line)
105     local s,e,ver,fmt,i,v
106
107     -- grabs the version, and the format version
108     s,e,ver,fmt = string.find(line, "-- Version: ([%d%a%.%-]+) File Format: (%d+)%s-$")
109
110     if ver == nil or fmt == nil then
111         print("*** xp module error: malformed Version string in /"..file_name.." line 1:")
112         print(line)
113         os.exit()
114     end
115
116     if tonumber(fmt) == 1 then
117         return parselinev1
118     else
119         print("*** xp module error: unsupported file format in /"..file_name.." file: "..fmt)
120         print("*** the version of this mod is: "..xpmodver)
121         print("*** this version only supports these format versions: ")
122         for i,v in ipairs(ffversupport) do
123             print(v)
124         end
125         print("***")
126         os.exit()
127     end
128 end
129
130 -- read in the xp and levels for each player listed in the xplevels_file
131 -- store the stats in xplevels, the table.
132 local function loadxp()
133     local input, line, parsel, name, stats
134
135     input = io.open(xplevels_file, "r")
136
137     if input ~= nil then
138         line = input:read("*l")
139
140         if line ~= nil then
141             parsel = checkfver(line)
142
143             line = input:read("*l")
144             while line ~= nil do
145
146                 name, stats = parsel(line)
147                 if name ~= nil then
148                     xplevels[name] = stats
149                 end
150
151                 line = input:read("*l")
152             end
153         end
154         io.close(input)
155     end
156 end
157
158 local function get_time()
159     return os.time()
160 end
161
162 -- returns true if player has ever logged into the server
163 -- it's different than minetest.is_player() since it accepts the name as a string,
164 -- and because you don't need to run a function that returns a player obj only for *online* players.
165 local function is_player(name)
166     local file
167
168     if playert[name] == nil then
169         file = io.open(minetest.get_worldpath().."/players/"..name, "r")
170         if file ~= nil then
171             playert[name] = true
172             io.close(file)
173         else
174             playert[name] = false
175         end
176     end
177
178     return playert[name]
179 end
180
181 -- returns the number of xp required to gain the next level.
182 -- pass it the player's current level.
183 local function lastpoint(level)
184     local i
185
186     if lptable == nil then
187         lptable = {}
188         for i = 0,max_level - 1 do
189             lptable[i] = 2 ^ i
190         end
191     end
192     return lptable[level]
193 end
194
195 -- returns true if a player is at the maximum level.
196 -- pass it the player's level for a given skill.
197 local function ismaxlvl(level)
198     return max_level ~= 0 and level >= max_level
199 end
200
201 -- initializes a player's experience points and level for a given skill
202 local function initpxp(name, skill)
203     if xplevels[name] == nil then
204         xplevels[name] = {}
205     end
206     if xplevels[name][skill] == nil then
207         xplevels[name][skill] = {xp = 0, level = 0}
208     end
209 end
210
211 -- gives npoints additional xp points for the player in the specified skillset
212 local function gainxp(name, skill, npoints)
213     local stat, appnd, gainedlevel
214
215     if npoints < 0 then
216         return "negative"
217     end
218
219     if is_player(name) == false then
220         return "non-player"
221     end
222
223     initpxp(name, skill)
224     stat = xplevels[name][skill]
225
226     if ismaxlvl(stat.level) then
227         return "level-max"
228     end
229
230     -- the file needs to be updated in the future:
231     changed = true
232
233     gainedlevel = false
234     while npoints > 0 do
235
236         if stat.xp + npoints < lastpoint(stat.level) then
237             stat.xp = stat.xp + npoints
238             if gainedlevel == true then
239                 break
240             else
241                 return "gained-xp"
242             end
243         else
244             npoints = npoints - (lastpoint(stat.level) - stat.xp)
245
246             stat.level = stat.level + 1
247             stat.xp = 0
248
249             if ismaxlvl(stat.level) then
250                 appnd = " (max)"
251                 break
252             else
253                 appnd = ""
254             end
255
256             gainedlevel = true
257         end
258     end
259     minetest.chat_send_player(name, "Welcome to level "..stat.level.." in "..skill.."!"..appnd)
260     minetest.log("action", "xp mod: "..name.." gained level "..stat.level.." in "..skill..appnd..".")
261
262     return "gained-level"
263 end
264
265 -- prints the level and xp for a player in the specified skillset.
266 -- if no skillset is selected, then stats for every skillset are printed.
267 -- output goes to the player's chat console.
268 local function printxp(name, skill)
269     local i,v,stat,appnd
270
271     if xplevels[name] == nil then
272         minetest.chat_send_player(name, "You have no experience in anything.")
273         return
274     end
275
276     if skill == "" then
277         for i,v in pairs(xplevels[name]) do
278             if i ~= "" then
279                 printxp(name, i)
280             end
281         end
282     else
283         stat = xplevels[name][skill]
284         if stat == nil then
285             minetest.chat_send_player(name, "You don't have experience in '"..skill.."'. (check your spelling.)")
286         else
287             initpxp(name, skill)
288
289             if ismaxlvl(stat.level) then
290                 appnd = ". (max)"
291             else
292                 appnd = ", plus "..stat.xp.."/"..lastpoint(stat.level).." experience points."
293             end
294             minetest.chat_send_player(name, "You are level "..stat.level.." in "..skill..appnd)
295         end
296     end
297 end
298
299 -- save user xp and levels to xplevels_file
300 local function savetofile()
301     local line, output, i, v, j, w
302
303     output = io.open(xplevels_file, "w")
304
305     output:write("-- Version: "..xpmodver.." File Format: "..ffver.."\n")
306
307     for i, v in pairs(xplevels) do
308         line = ""
309         for j, w in pairs(v) do
310             line = line.." ("..j..": level: "..w.level.." xp: "..w.xp..")"
311         end
312         output:write(i..line.."\n")
313     end
314
315     io.close(output)
316 end
317
318 ----------------------------------
319
320 -- callback registration:
321 ----------------------------------
322
323 -- minetest.register_privilege("givexp", "Can give xp to others with /givexp"
324
325 -- it raises the xp of a player by the specified number of points.
326 minetest.register_chatcommand("givexp", {
327     params = "<username> <skill> <num-xp>",
328     description = "Give a player experience points",
329     -- privs = {givexp=true},
330     privs = {},
331     func = function(caller, param)
332         local ign, name, skill, numxp, result
333
334         -- matches two words: the name and the skill
335         ign,ign,name,skill,numxp = string.find(param, "^([^%s]+)%s+([^%s]+)%s+(%d+)%s*$")
336
337         if numxp == nil then
338             minetest.chat_send_player(caller, "Incorrect usage. See: /help givexp")
339         else
340             result = gainxp(name,skill,tonumber(numxp))
341             if result == "non-player" then
342                 minetest.chat_send_player(caller, name.." isn't a player.")
343             elseif result == "level-max" then
344                 minetest.chat_send_player(caller, name.."'s "..skill.." level is already maxed-out.")
345             elseif result == "gained-xp" or result == "gained-level" then
346                 minetest.chat_send_player(caller, name.." is now level "..xplevels[name][skill].level.." plus "..xplevels[name][skill].xp.." xp in "..skill..".")
347                 minetest.log("action", "xp mod: ("..caller.." gave "..name.." "..numxp.." xp in "..skill..".)")
348             end
349         end
350     end,
351 })
352
353 -- shows the xp for the calling player, for a given skill, or if none is listed, then all of them.
354 minetest.register_chatcommand("showxp", {
355     params = "<skill (opt)>",
356     description = "Show your experience points",
357     -- privs = {xp=true},
358     privs = {},
359     func = printxp,
360 })
361
362 -- every time a player digs an object, they get one extra xp
363 minetest.register_on_dignode(function(pos, node, player)
364     local name = player:get_player_name()
365     gainxp(name, "digging", 1)
366 end)
367
368
369 -- every time a player joins the game, add their name to the list of existing players.
370 -- if their player file wasn't readable, this works around the issue. also, if this is a new player,
371 -- then they will be added to the table even if they were previously marked as non-existent.
372 minetest.register_on_joinplayer(function(player)
373     local name = player:get_player_name()
374     playert[name] = true
375 end)
376
377 local delta = 0
378 -- every save_delta seconds, save the xp and levels, if they have changed for one or more players
379 minetest.register_globalstep(function(dtime)
380     delta = delta + dtime
381     -- save it every <save_delta> seconds
382     if delta > save_delta then
383         delta = delta - save_delta
384         if changed then
385             savetofile()
386             changed = false
387         end
388     end
389 end)
390
391 ----------------------------------
392
393 -- execute at the start of the game:
394 ----------------------------------
395
396 -- before this file is done being read, this function must be called
397 -- to load player xp and levels for each skill
398 loadxp()
399