vrtc / flowerpicker (public) (License: GPLv3) (since 2019-11-07) (hash sha1)
Flowerpicker is a GUI extension, that is an add-on, for World of Warcraft game client. It is under development and not yet ready for usage by players.
List of commits:
Subject Hash Author Date (UTC)
Initial commit f014031ddd4b2422ef5d000f33ad6602e64c9b36 Vladyslav Bondarenko 2019-11-05 20:29:14
Commit f014031ddd4b2422ef5d000f33ad6602e64c9b36 - Initial commit
The project started. The basic functionality, that is logging
of corpse looting, is present.
An attempt was made to mimic Java naming conventions with files,
project names and types, as to make it more understandeable.
The deployed add-on directoty must also be possible to use
as a workspace directory. This is done to keep it simple and
to leverage Lua interpreted language better by avoiding
build and deployment steps.

There is a problem with dating written events. Originally the realm name
was supposed to substitute the need for the time zone.
However, since the date and time are from the client's OS
time provider, realm name becomes useless in determining
the written event's timezone.

There is a need to add logging of game zone and game subzone to
an event that is to be written down.

The most significant yet problem is how to determinte the source of
loot.
Author: Vladyslav Bondarenko
Author date (UTC): 2019-11-05 20:29
Committer name: Vladyslav Bondarenko
Committer date (UTC): 2019-11-05 20:29
Parent(s):
Signer:
Signing key:
Signing status: N
Tree: 815f5be3eed7dd10431038d7e008eb2a639a349b
File Lines added Lines deleted
Flowerpicker.lua 347 0
flowerpicker.toc 6 0
File Flowerpicker.lua added (mode: 100644) (index 0000000..3f5c528)
1 --[[ Security, reliability. ]]--
2
3 local function sanitiseShort(supposedNumber)
4 local sane = nil
5 if nil == supposedNumber then
6 sane = nil
7 elseif 'number' == type(supposedNumber) then
8 sane = math.min(math.max(-32768, math.ceil(supposedNumber)), 32767)
9 else
10 sane = nil
11 end
12 return sane
13 end
14
15 local function sanitiseTimestamp(supposedTimestamp)
16 local sane = nil
17 if nil == supposedTimestamp then
18 elseif 'string' == type(supposedTimestamp) then
19 sane = string.sub(strtrim(supposedTimestamp), 1, 19)
20 else
21 sane = nil
22 end
23 return sane
24 end
25
26 local function sanitiseAnyName(supposedName)
27 local sane = nil
28 if nil == supposedName then
29 sane = nil
30 elseif 'string' == type(supposedName) then
31 sane = string.sub(strtrim(supposedName), 1, 256)
32 else
33 sane = nil
34 end
35 return sane
36 end
37
38 local function validateAnyName(supposedName, lengthMin, lengthMax)
39 if nil == lengthMin then
40 lengthMin = 2
41 elseif 'number' ~= type(lengthMin) then
42 lengthMin = 2
43 elseif lengthMin < 0 then
44 lengthMin = 2
45 elseif lengthMin > 256 then
46 lengthMin = 256
47 end
48 if nil == lengthMax then
49 lengthMax = 32
50 elseif 'number' ~= type(lengthMin) then
51 lengthMax = 32
52 elseif lengthMax < 0 then
53 lengthMax = 32
54 elseif lengthMax > 256 then
55 lengthMax = 256
56 end
57 assert (lengthMin <= lengthMax, 'Illegal argument.')
58 local isValid = true
59 local errorMsg = nil
60
61 isValid = supposedName ~= nil and isValid
62 if not isValid then
63 errorMsg = 'Null pointer.'
64 return isValid, errorMsg
65 end
66
67 isValid = 'string' == type(supposedName) and isValid
68 if not isValid then
69 errorMsg = 'Not a string.'
70 return isValid, errorMsg
71 end
72
73 local len = string.len(supposedName)
74 isValid = (len >= lengthMin or len <= lengthMax) and isValid
75 if not isValid then
76 errorMsg = string.format('String length %d not in [%d, %d].', len, lengthMin, lengthMax)
77 return isValid, errorMsg
78 end
79
80 return isValid, errorMsg
81 end
82
83 local function validateTimestamp(t)
84 local isValid = true
85 local errorMsg = nil
86
87 isValid, errorMsg = validateAnyName(t, 19, 19)
88 if not isValid then
89 return isValid, errorMsg
90 end
91
92 --[[
93 -- os.date('%Y-%m-%d %H:%M:%S', os.time())
94 -- ex, 2019-11-05 14:28:30
95 ]]--
96 isValid = t == string.match(t, '%d%d%d%d--%d%d--%d%d %d%d:%d%d:%d%d') and isValid
97 if not isVallid then
98 errorMsg = 'Is not a timestamp.'
99 return isValid, errorMsg
100 end
101
102 return isValid, errorMsg
103 end
104
105 local function validatePositiveAndNonZero(n)
106 local isValid = true
107 local errorMsg = nil
108
109 isValid = n ~= nil and isValid
110 if not isValid then
111 errorMsg = 'Null pointer.'
112 return isValid, errorMsg
113 end
114
115 isValid = 'number' == type(n) and isValid
116 if not isValid then
117 errorMsg = 'Not a number.'
118 return isValid, errorMsg
119 end
120
121 isValid = n > 0 and isValid
122 if not isValid then
123 errorMsg = 'Is negative or zero.'
124 return isValid, errorMsg
125 end
126
127 return isValid, errorMsg
128 end
129
130 local function validateEnum(element, permissibleValueSet)
131 assert (nil ~= permissibleValueSet)
132 assert ('table' == type(permissibleValueSet))
133 local p = #permissibleValueSet
134 if nil == p then
135 p = 0
136 elseif 'number' ~= type(p) then
137 p = 0
138 end
139 local isValid = false
140 local i = 1
141 local j = math.min(math.max(0, math.floor(p)), 1024)
142 while (i <= j and not isValid) do
143 isValid = (element == permissibleValueSet[i]) or isValid
144 i = i + 1
145 end
146 return isValid, 'Illegal value in an enumeration.'
147 end
148
149 --[[ Business. ]]--
150
151 local function getPermissibleHarvestTypeSet()
152 return {
153 'COOKING',
154 'FISHING',
155 'HERBALISM',
156 'LOOTCORPSE',
157 'MINING',
158 'OPENING'
159 }
160 end
161
162 local function isPermissibleSpellToProduceLoot(spellName)
163 local n = sanitiseAnyName(spellName)
164 if nil == n then
165 return false
166 end
167 assert (validateAnyName(n))
168 local result = false
169
170 if 'Fishing' == n then
171 result = true
172 elseif 'Herbalism' == n then
173 result = true
174 end
175
176 assert (result ~= nil)
177 assert ('boolean' == type(result))
178 return result
179 end
180
181 local function createEventHarvest(harvestTypeDesignation, realmName, harvesterName, sourceName, harvestTimestamp, harvestedItemName, harvestedItemQuantity)
182 local d = sanitiseAnyName(harvestTypeDesignation)
183 local r = sanitiseAnyName(realmName)
184 local c = sanitiseAnyName(harvesterName)
185 local s = sanitiseAnyName(sourceName)
186 local t = sanitiseTimestamp(harvestTimestamp)
187 local i = sanitiseAnyName(harvestedItemName)
188 local q = sanitiseShort(harvestedItemQuantity)
189 assert (validateEnum(d, getPermissibleHarvestTypeSet()))
190 assert (validateAnyName(r))
191 assert (validateAnyName(c))
192 assert (validateAnyName(s))
193 assert (validateTimestamp(t))
194 assert (validateAnyName(i))
195 assert (validatePositiveAndNonZero(q))
196
197 local event = {
198 d, r, c, s, t, i, q
199 }
200
201 assert (event ~= nil)
202 assert ('table' == type(event))
203 return event
204 end
205
206 local function mapSpellNameToHarvestTypeDesignation(spellName)
207 local s = sanitiseAnyName(spellName)
208 assert (validateAnyName(s))
209 local t = nil
210
211 if 'Herbalism' == s then
212 t = 'HERBALISM'
213 elseif 'Fishing' == s then
214 t = 'FISHING'
215 else
216 t = string.upper(s)
217 end
218
219 assert (validateEnum(s, getPermissibleHarvestTypeSet()))
220 return t
221 end
222
223 --[[ Input. ]]--
224
225 --[[ Main. ]]--
226
227 local function updateLootCache()
228 FlowerpickerLootCache = {}
229 local lootCache = FlowerpickerLootCache
230 assert (lootCache ~= nil)
231 assert ('table' == type(lootCache))
232 local n = GetNumLootItems()
233 if nil == n then
234 n = 0
235 elseif 'number' ~= type(n) then
236 n = 0
237 end
238 local i = 1
239 local j = math.min(math.max(0, math.floor(n)), 1024)
240 while (i <= j) do
241 lootCache[i] = {GetLootSlotInfo(i)}
242 i = i + 1
243 end
244 end
245
246 local function main(self, event, arg1, arg2, arg3, arg4, arg5)
247 assert (self ~= nil)
248 assert ('table' == type(self))
249 assert (self.api ~= nil)
250 assert ('table' == type(self.api))
251 assert (event ~= nil)
252 assert ('string' == type(event))
253
254 if 'UNIT_SPELLCAST_SUCCEEDED' == event then
255 local unitId = sanitiseAnyName(arg1)
256 if 'player' == unitId then
257 local spellName = sanitiseAnyName(arg2)
258 self.api.lastSpellCastName = spellName
259 end
260 elseif 'LOOT_OPENED' == event then
261 updateLootCache()
262 self.api.lootedCorpseGuessedName = sanitiseAnyName(UnitName('target'))
263 elseif 'LOOT_SLOT_CLEARED' == event then
264 local slotId = sanitiseShort(arg1)
265 assert (slotId ~= nil)
266 assert ('number' == type(slotId))
267 local spellName = sanitiseAnyName(self.api.lastSpellCastName)
268 local d
269 local s
270 if isPermissibleSpellToProduceLoot(spellName) then
271 assert (validateAnyName(spellName))
272 d = mapSpellNameToHarvestTypeDesignation(spellName)
273 else
274 d = 'LOOTCORPSE'
275 s = sanitiseAnyName(self.api.lootedCorpseGuessedName)
276 assert (validateAnyName(s))
277 end
278 assert (validateEnum(d, getPermissibleHarvestTypeSet()))
279 local r = sanitiseAnyName(GetRealmName())
280 assert (validateAnyName(r))
281 local p = sanitiseAnyName(UnitName('player'))
282 assert (validateAnyName(p))
283 local t = date('%Y-%m-%d %H:%M:%S', time())
284 local lootCache = FlowerpickerLootCache
285 assert (lootCache ~= nil)
286 assert ('table' == type(lootCache))
287 local lootedItem = lootCache[slotId]
288 assert (lootedItem ~= nil)
289 assert ('table' == type(lootedItem))
290 local i = sanitiseAnyName(lootedItem[2])
291 assert (validateAnyName(i))
292 local q = sanitiseShort(lootedItem[3])
293 assert (validatePositiveAndNonZero(q))
294 local e = createEventHarvest(d, r, p, s, t, i, q)
295 table.insert(FlowerpickerSavedVariables, e)
296 print(d, r, p, s, t, i, q)
297 --updateLootCache()
298 --[[local flowerpickerEvent = createEventHarvest(
299 )
300 FlowerpickerSavedVariables[] = flowerpickerEvent]]--
301 elseif 'LOOT_SLOT_CHANGED' == event then
302 print(event, arg1, arg2, arg3)
303 elseif 'LOOT_CLOSED' == event then
304 FlowerpickerLootCache = {}
305 self.api.lootedCorpseGuessedName = nil
306 else
307 error('Unknown event encountered and ignored "' .. event .. '".')
308 end
309 end
310
311 local function init()
312 local addonFrame = FlowerpickerAddOnFrame
313 assert (nil ~= addonFrame)
314 assert ('table' == type(addonFrame))
315
316 if nil == FlowerpickerSavedVariables or 'table' ~= type(FlowerpickerSavedVariables) then
317 FlowerpickerSavedVariables = {}
318 end
319 local dao = FlowerpickerSavedVariables
320 assert (nil ~= dao)
321 assert ('table' == type(dao))
322
323 if nil == FlowerpickerLootCache or 'table' ~= type(FlowerpickerLootCache) then
324 FlowerpickerLootCache = {}
325 end
326 local lootCache = FlowerpickerLootCache
327 assert (nil ~= lootCache)
328 assert ('table' == type(lootCache))
329
330 addonFrame:UnregisterEvent('ADDON_LOADED')
331 addonFrame:RegisterEvent('UNIT_SPELLCAST_SUCCEEDED')
332 addonFrame:RegisterEvent('LOOT_OPENED')
333 addonFrame:RegisterEvent('LOOT_SLOT_CLEARED')
334 addonFrame:RegisterEvent('LOOT_CLOSED')
335 addonFrame:SetScript('OnEvent', main)
336
337 local api = {
338 ['createEventHarvest'] = createEventHarvest,
339 ['lootCache'] = lootCache,
340 ['lastSpellCastName'] = nil
341 }
342 addonFrame.api = api
343 end
344
345 local frame = CreateFrame('FRAME', 'FlowerpickerAddOnFrame', UIParent)
346 frame:RegisterEvent('ADDON_LOADED')
347 frame:SetScript('OnEvent', init)
File flowerpicker.toc added (mode: 100644) (index 0000000..7b1ea13)
1 ##Interface: 30300
2 ##Title: Flowerpicker
3 ##Version: 0.0.0wip
4 ##Notes: Harvest history tracker.
5 ##SavedVariables: FlowerpickerSavedVariables, FlowerpickerLootCache
6 Flowerpicker.lua
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/vrtc/flowerpicker

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/vrtc/flowerpicker

Clone this repository using git:
git clone git://git.rocketgit.com/user/vrtc/flowerpicker

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main