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