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