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)
Update registration of money operations f0b4ec71099900dd51b5a4c35ce688586d6ddbfe Vladyslav Bondarenko 2019-11-06 19:49:01
Add basic money income via looting registration 9cfb08138268bb6985db1729c160b444a5e4e56a Vladyslav Bondarenko 2019-11-06 14:07:53
Update event format 554e63f3490dc48befe37fbfacf5c8c9ad188451 Vladyslav Bondarenko 2019-11-06 09:59:18
Initial commit f014031ddd4b2422ef5d000f33ad6602e64c9b36 Vladyslav Bondarenko 2019-11-05 20:29:14
Commit f0b4ec71099900dd51b5a4c35ce688586d6ddbfe - Update registration of money operations
The event handling code is changed to be more predictable.

The add-on is prepared to register money operations with merchants
in the game. However, no actual registration is being made yet.
Still, only money income from looting corpses is registered,
and other money operations implementation is postponed.

There is data corruption occuring when a player loots a container.
When a player loots a container, then it is counted as if they
looted a corpse of the last defeated enemy, instead of harvesting
a resource node, as it is supposed to be.
Author: Vladyslav Bondarenko
Author date (UTC): 2019-11-06 19:49
Committer name: Vladyslav Bondarenko
Committer date (UTC): 2019-11-06 19:49
Parent(s): 9cfb08138268bb6985db1729c160b444a5e4e56a
Signing key:
Tree: 40b350a20e36897b1d9a29c03f08ac5a2fcd3ccf
File Lines added Lines deleted
Flowerpicker.lua 198 52
File Flowerpicker.lua changed (mode: 100644) (index 54d9d6d..f617c8e)
... ... local function sanitiseShort(supposedNumber)
49 49 return sane return sane
50 50 end end
51 51
52 local function sanitiseInteger(supposedNumber)
53 local sane = nil
54 if nil == supposedNumber then
55 sane = nil
56 elseif 'number' == type(supposedNumber) then
57 sane = math.min(math.max(-2147483648, math.ceil(supposedNumber)), 2147483647)
58 else
59 sane = nil
60 end
61 return sane
62 end
63
52 64 local function sanitiseTimestamp(supposedTimestamp) local function sanitiseTimestamp(supposedTimestamp)
53 65 local sane = nil local sane = nil
54 66 if nil == supposedTimestamp then if nil == supposedTimestamp then
 
... ... local function isCachedItemCoin(lootCache, slotId)
330 342 return true == itemInfo[6] return true == itemInfo[6]
331 343 end end
332 344
333 local function processLoot(lootCache, slotId, lastSpellCastName, lootedCorpseGuessedName, moneyDiff)
345 local function persistEventHarvest(event)
346 assert (event ~= nil)
347 assert ('table' == type(event))
348 local dao = FlowerpickerSavedVariables
349 assert (dao ~= nil)
350 assert ('table' == type(dao))
351 table.insert(dao, event)
352 end
353
354 local function createEventHarvestWithDefaults(eventTypeDesignation, sourceName, itemName, itemQuantity)
355 --[[ begin defaults ]]--
356 local r = sanitiseAnyName(GetRealmName())
357 assert (validateAnyName(r))
358
359 local p = sanitiseAnyName(UnitName('player'))
360 assert (validateAnyName(p))
361
362 local t = date('%Y-%m-%d %H:%M:%S', time()) .. get_tzoffset(get_timezone())
363 assert (validateTimestamp(t))
364
365 local z = sanitiseAnyName(GetZoneText())
366 if nil == z or string.len(z) <= 0 then
367 z = 'Unknown Zone'
368 end
369 assert (validateAnyName(z))
370
371 local sz = sanitiseAnyName(GetSubZoneText())
372 if nil == sz or string.len(sz) <= 0 then
373 sz = 'Unknown Subzone'
374 end
375 assert (validateAnyName(sz))
376 --[[ end defaults ]]--
377
378 --[[ begin explicit ]]--
379 local d = sanitiseAnyName(eventTypeDesignation)
380 assert (validateEnum(d, getPermissibleHarvestTypeSet()))
381
382 local s = sanitiseAnyName(sourceName)
383 assert (validateAnyName(s))
384
385 local i = sanitiseAnyName(itemName)
386 assert (validateAnyName(i))
387
388 local q = sanitiseShort(itemQuantity)
389 assert (validatePositiveAndNonZero(q))
390 --[[ end explicit ]]--
391
392 local e = createEventHarvest(r, p, z, sz, t, d, s, i, q)
393 assert (e ~= nil)
394 return e
395 end
396
397 local function registerLootGainFromCorpse(itemName, itemQuantity, corpseName)
398 local n = sanitiseAnyName(itemName)
399 assert (validateAnyName(n))
400 local s = sanitiseAnyName(corpseName)
401 assert (validateAnyName(s))
402 local q = sanitiseShort(itemQuantity)
403 assert (validatePositiveAndNonZero(q))
404
405 local e = createEventHarvestWithDefaults('LOOTCORPSE', s, n, q)
406 persistEventHarvest(e)
407 end
408
409 local function registerMoneyGainFromCorpse(moneyAmount, corpseName)
410 local m = sanitiseInteger(moneyAmount)
411 assert (validatePositiveAndNonZero(m))
412 local s = sanitiseAnyName(corpseName)
413 assert (validateAnyName(s))
414
415 local e = createEventHarvestWithDefaults('LOOTCORPSE', s, 'Money', m)
416 persistEventHarvest(e)
417 end
418
419 local function processLoot(lootCache, slotId, lastSpellCastName, lootedCorpseGuessedName, playerMoneyDiff)
334 420 local spellName = sanitiseAnyName(lastSpellCastName) local spellName = sanitiseAnyName(lastSpellCastName)
335 421 local d local d
336 422 local s local s
 
... ... local function processLoot(lootCache, slotId, lastSpellCastName, lootedCorpseGue
364 450 local q local q
365 451 if isCachedItemCoin(lootCache, slotId) then if isCachedItemCoin(lootCache, slotId) then
366 452 --[[ Potentially error prone! ]]-- --[[ Potentially error prone! ]]--
367 q = sanitiseShort(moneyDiff)
453 q = sanitiseShort(playerMoneyDiff)
368 454 else else
369 455 q = sanitiseShort(lootedItem[3]) q = sanitiseShort(lootedItem[3])
370 456 end end
 
... ... local function processLoot(lootCache, slotId, lastSpellCastName, lootedCorpseGue
381 467 table.insert(FlowerpickerSavedVariables, e) table.insert(FlowerpickerSavedVariables, e)
382 468 end end
383 469
470 local function handleLootSlotCleared(lootCache, slotId, someSpellName, someCorpseName)
471 assert (lootCache ~= nil)
472 assert ('table' == type(lootCache))
473 assert (slotId ~= nil)
474 assert ('number' == type(slotId))
475
476 local sp = sanitiseAnyName(someSpellName)
477 local cr = sanitiseAnyName(someCorpseName)
478
479 if isCachedItemCoin(lootCache, slotId) then
480 --[[ See `handlePlayerMoney` function. ]]--
481 return
482 elseif isPermissibleSpellToProduceLoot(sp) then
483 error('TODO')
484 elseif true == validateAnyName(cr) then
485 local lootedItem = getCachedItemInfo(lootCache, slotId)
486 assert (lootedItem ~= nil)
487 assert ('table' == type(lootedItem))
488
489 i = sanitiseAnyName(lootedItem[2])
490 assert (validateAnyName(i))
491 q = sanitiseShort(lootedItem[3])
492 assert (validatePositiveAndNonZero(q))
493
494 registerLootGainFromCorpse(i, q, cr)
495 else
496 error('Failed to process loot for an unknown reason.')
497 end
498 end
499
500 local function handlePlayerMoney(playerMoneyBefore, playerMoneyAfter, playerMoneyDiff, corpseName, gossiperName, merchantName)
501 local isMoneyGained = playerMoneyAfter > playerMoneyBefore and playerMoneyDiff > 0
502 local isMoneyLost = playerMoneyBefore > playerMoneyAfter and playerMoneyDiff < 0
503
504 if gossiperName ~= nil then
505 print('Gossip with ' .. gossiperName .. 'resulted in a money operation.')
506 elseif merchantName ~= nil then
507 print('A money operation with merchant ' .. merchantName .. ' occurred.')
508 elseif isMoneyGained and not isMoneyLost and corpseName ~= nil then
509 local m = sanitiseInteger(playerMoneyDiff)
510 assert (validatePositiveAndNonZero(m))
511 local cr = sanitiseAnyName(corpseName)
512 assert (validateAnyName(cr))
513 registerMoneyGainFromCorpse(m, cr)
514 else
515 print('Unaccounted money operation encountered.')
516 end
517 end
518
384 519 local function main(self, event, arg1, arg2, arg3, arg4, arg5) local function main(self, event, arg1, arg2, arg3, arg4, arg5)
385 520 assert (self ~= nil) assert (self ~= nil)
386 521 assert ('table' == type(self)) assert ('table' == type(self))
 
... ... local function main(self, event, arg1, arg2, arg3, arg4, arg5)
397 532 end end
398 533 elseif 'LOOT_OPENED' == event then elseif 'LOOT_OPENED' == event then
399 534 updateLootCache(FlowerpickerLootCache) updateLootCache(FlowerpickerLootCache)
400 self.api.lootedCorpseGuessedName = sanitiseAnyName(UnitName('target'))
535 if (1 == UnitIsDead('target')) then
536 self.api.lastLootedCorpseName = sanitiseAnyName(UnitName('target'))
537 end
401 538 self.api.isLooting = true self.api.isLooting = true
402 539 elseif 'LOOT_SLOT_CLEARED' == event then elseif 'LOOT_SLOT_CLEARED' == event then
403 540 local slotId = sanitiseShort(arg1) local slotId = sanitiseShort(arg1)
541 assert (slotId ~= nil)
542 assert ('number' == type(slotId))
404 543 local lootCache = FlowerpickerLootCache local lootCache = FlowerpickerLootCache
405 544 assert (lootCache ~= nil) assert (lootCache ~= nil)
406 545 assert ('table' == type(lootCache)) assert ('table' == type(lootCache))
407 self.api.lootSlotId = slotId
408 if isCachedItemCoin(lootCache, slotId) then
409 return
410 end
411 assert (slotId ~= nil)
412 assert ('number' == type(slotId))
413 546 local lastSpellCastName = sanitiseAnyName(self.api.lastSpellCastName) local lastSpellCastName = sanitiseAnyName(self.api.lastSpellCastName)
414 local corpseName = sanitiseAnyName(self.api.lootedCorpseGuessedName)
415 processLoot(FlowerpickerLootCache, slotId, lastSpellCastName, corpseName)
547 local corpseName = sanitiseAnyName(self.api.lastLootedCorpseName)
548 self.api.lootSlotId = slotId
549 handleLootSlotCleared(lootCache, slotId, lastSpellCastName, corpseName)
416 550 elseif 'LOOT_CLOSED' == event then elseif 'LOOT_CLOSED' == event then
417 551 clearLootCache(FlowerpickerLootCache) clearLootCache(FlowerpickerLootCache)
418 552 self.api.lootSlotId = nil self.api.lootSlotId = nil
419 self.api.lastSpellCastName = nil
420 self.api.lootedCorpseGuessedName = nil
421 553 self.api.isLooting = false self.api.isLooting = false
554 elseif 'GOSSIP_SHOW' == event then
555 self.api.lastGossiperName = sanitiseAnyName(UnitName('target'))
556 self.api.isGossiping = true
557 elseif 'GOSSIP_CLOSED' == event then
558 self.api.isGossiping = false
422 559 elseif 'PLAYER_MONEY' == event then elseif 'PLAYER_MONEY' == event then
423 local updatedMoney = GetMoney()
424 assert (updatedMoney ~= nil)
425 assert ('number' == type(updatedMoney))
426 assert (updatedMoney >= 0)
427 local playerMoney = self.api.playerMoney
428 assert (playerMoney ~= nil)
429 assert ('number' == type(playerMoney))
430 assert (playerMoney >= 0)
431 local moneyDiff = updatedMoney - playerMoney
432 assert (moneyDiff ~= nil)
433 assert ('number' == type(moneyDiff))
434 --[[ Only register money income for now. ]]--
435 if (moneyDiff > 0 and true == self.api.isLooting) then
436 local slotId = self.api.lootSlotId
437 local lastSpellCastName = sanitiseAnyName(self.api.lastSpellCastName)
438 local corpseName = sanitiseAnyName(self.api.lootedCorpseGuessedName)
439 assert (slotId ~= nil and (lastSpellCastName ~= nil or corpseName ~= nil), 'Unknown money source.')
440
441 --[[
442 -- There is a limit to item quantity per registered loot event.
443 -- Therefore, split potentially larger money amount between multiple events.
444 ]]--
445 local i = 0
446 local m = moneyDiff
447 local maxItemQuantityPerEvent = 32767
448 local maxEvents = 8
449 local lootCache = FlowerpickerLootCache
450 assert (lootCache ~= nil)
451 assert ('table' == type(lootCache))
452 while (m > maxItemQuantityPerEvent and i < maxEvents) do
453 processLoot(lootCache, slotId, lastSpellCastName, corpseName, maxItemQuantitiyPerEvent)
454 m = m - maxItemQuantityPerEvent
455 i = i + 1
456 end
457 m = moneyDiff - maxItemQuantityPerEvent*i
458 processLoot(lootCache, slotId, lastSpellCastName, corpseName, m)
560 local playerMoneyAfter = GetMoney()
561 assert (validatePositiveAndNonZero(playerMoneyAfter))
562
563 local playerMoneyBefore = sanitiseInteger(self.api.playerMoney)
564 assert (playerMoneyBefore >= 0)
565
566 local playerMoneyDiff = playerMoneyAfter - playerMoneyBefore
567
568 self.api.playerMoney = playerMoneyAfter
569
570 local corpseName = sanitiseAnyName(self.api.lastLootedCorpseName)
571
572 local gossiperName
573 if true == self.api.isGossiping then
574 gossiperName = sanitiseAnyName(self.api.lastGossiperName)
575 else
576 gossiperName = nil
459 577 end end
460 self.api.playerMoney = updatedMoney
578
579 local merchantName
580 if true == self.api.isShopping then
581 merchantName = sanitiseAnyName(self.api.lastMerchantName)
582 else
583 merchantName = nil
584 end
585
586 handlePlayerMoney(playerMoneyBefore, playerMoneyAfter, playerMoneyDiff, corpseName, gossiperName, merchantName)
461 587 else else
462 588 error('Unknown event encountered and ignored "' .. event .. '".') error('Unknown event encountered and ignored "' .. event .. '".')
463 589 end end
 
... ... local function init()
487 613 addonFrame:RegisterEvent('LOOT_OPENED') addonFrame:RegisterEvent('LOOT_OPENED')
488 614 addonFrame:RegisterEvent('LOOT_SLOT_CLEARED') addonFrame:RegisterEvent('LOOT_SLOT_CLEARED')
489 615 addonFrame:RegisterEvent('LOOT_CLOSED') addonFrame:RegisterEvent('LOOT_CLOSED')
616 addonFrame:RegisterEvent('GOSSIP_SHOW')
617 addonFrame:RegisterEvent('GOSSIP_CLOSED')
490 618 addonFrame:RegisterEvent('PLAYER_MONEY') addonFrame:RegisterEvent('PLAYER_MONEY')
491 619 addonFrame:SetScript('OnEvent', main) addonFrame:SetScript('OnEvent', main)
492 620
493 621 local api = { local api = {
494 622 ['createEventHarvest'] = createEventHarvest, ['createEventHarvest'] = createEventHarvest,
495 623 ['lootCache'] = lootCache, ['lootCache'] = lootCache,
496 ['lootedCorpseGuessedName'] = nil,
624 ['lastLootedCorpseName'] = nil,
497 625 ['lastSpellCastName'] = nil, ['lastSpellCastName'] = nil,
626 ['lastMerchantName'] = nil,
498 627 ['isLooting'] = false, ['isLooting'] = false,
499 ['playerMoney'] = GetMoney()
628 ['isShopping'] = false,
629 ['isGossiping'] = false,
630 ['playerMoney'] = sanitiseInteger(GetMoney())
500 631 } }
501 632 addonFrame.api = api addonFrame.api = api
633
634 MerchantFrame:HookScript('OnShow', function(...)
635 local f = FlowerpickerAddOnFrame
636 assert (f ~= nil)
637 f.api.isShopping = true
638 if (1 ~= UnitIsDead('target') and true ~= UnitPlayerControlled('target')) then
639 f.api.lastMerchantName = sanitiseAnyName(UnitName('target'))
640 end
641 end)
642
643 MerchantFrame:HookScript('OnHide', function(...)
644 local f = FlowerpickerAddOnFrame
645 assert (f ~= nil)
646 f.api.isShopping = false
647 end)
502 648 end end
503 649
504 650 local frame = CreateFrame('FRAME', 'FlowerpickerAddOnFrame', UIParent) local frame = CreateFrame('FRAME', 'FlowerpickerAddOnFrame', UIParent)
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