285 lines
10 KiB
Lua
285 lines
10 KiB
Lua
-- Microchip-chat 0.8.7: independent, ring-buffered per-channel history.
|
|
-- Original arrays and SavedVariables are migrated without discarding history.
|
|
-- Vanilla WoW 1.12 / Lua 5.0 compatible.
|
|
MCHistory = {}
|
|
|
|
local cachedChat, cachedCombat
|
|
local function invalidate()
|
|
cachedChat=nil
|
|
cachedCombat=nil
|
|
end
|
|
|
|
function MCHistory.Key(record)
|
|
if not record or record.legacy then return "LEGACY" end
|
|
if record.kind=="CHANNEL" then return record.channel or "CHANNEL" end
|
|
return record.kind or "LEGACY"
|
|
end
|
|
|
|
function MCHistory.Limit(store, defaultLimit, key)
|
|
local limits=store.historyLimits or {}
|
|
local value=limits[key]
|
|
if not value and string.sub(key or "",1,8)=="CHANNEL:" then
|
|
value=limits.CHANNEL
|
|
end
|
|
return tonumber(value) or tonumber(defaultLimit) or 1000
|
|
end
|
|
|
|
-- historyHeads[key] is the physical position of the oldest entry. The
|
|
-- array itself remains densely indexed for SavedVariables serialization.
|
|
-- Rotation at capacity only overwrites one slot, avoiding table.remove(1).
|
|
local function getRecord(bucket, head, index)
|
|
local n=table.getn(bucket)
|
|
if n==0 then return nil end
|
|
local physical=math.mod((head or 1)+index-2,n)+1
|
|
return bucket[physical]
|
|
end
|
|
|
|
function MCHistory.Get(store, key, index)
|
|
local bucket=store.historyByChannel and store.historyByChannel[key]
|
|
if not bucket then return nil end
|
|
return getRecord(bucket,(store.historyHeads or {})[key] or 1,index)
|
|
end
|
|
|
|
local function normalize(store, defaultLimit, key, bucket)
|
|
local n=table.getn(bucket)
|
|
local limit=MCHistory.Limit(store,defaultLimit,key)
|
|
local head=store.historyHeads[key] or 1
|
|
if n<=limit and (head==1 or n==limit) then return end
|
|
local first=math.max(1,n-limit+1)
|
|
local clean={}
|
|
local i
|
|
for i=first,n do
|
|
table.insert(clean,getRecord(bucket,head,i))
|
|
end
|
|
store.historyByChannel[key]=clean
|
|
store.historyHeads[key]=1
|
|
end
|
|
|
|
function MCHistory.Trim(store, defaultLimit)
|
|
if type(store.historyByChannel)~="table" then return end
|
|
if type(store.historyHeads)~="table" then store.historyHeads={} end
|
|
local key,bucket
|
|
for key,bucket in pairs(store.historyByChannel) do
|
|
if type(bucket)=="table" then
|
|
normalize(store,defaultLimit,key,bucket)
|
|
else
|
|
store.historyByChannel[key]=nil
|
|
store.historyHeads[key]=nil
|
|
end
|
|
end
|
|
invalidate()
|
|
end
|
|
|
|
local function migrateArray(store, records)
|
|
if type(records)~="table" then return end
|
|
local i,record,key,bucket
|
|
for i=1,table.getn(records) do
|
|
record=records[i]
|
|
if type(record)=="table" then
|
|
key=MCHistory.Key(record)
|
|
bucket=store.historyByChannel[key]
|
|
if not bucket then
|
|
bucket={}
|
|
store.historyByChannel[key]=bucket
|
|
end
|
|
table.insert(bucket,record)
|
|
end
|
|
end
|
|
end
|
|
|
|
function MCHistory.Initialize(store,defaultLimit)
|
|
if type(store.historyByChannel)~="table" then store.historyByChannel={} end
|
|
if type(store.historyLimits)~="table" then store.historyLimits={} end
|
|
if type(store.historyHeads)~="table" then store.historyHeads={} end
|
|
local key,value
|
|
for key,value in pairs(store.historyLimits) do
|
|
if type(key)~="string" or type(value)~="number" or value<50 or value>5000 then
|
|
store.historyLimits[key]=nil
|
|
else
|
|
store.historyLimits[key]=math.floor(value)
|
|
end
|
|
end
|
|
if not store.historyMigrated085 then
|
|
migrateArray(store,store.entries)
|
|
migrateArray(store,store.combatEntries)
|
|
store.entries=nil
|
|
store.combatEntries=nil
|
|
store.historyMigrated085=true
|
|
end
|
|
-- The oldest pre-sequence legacy records may not have sequence values.
|
|
-- Give them stable, unique negative IDs so cursor paging works for them.
|
|
local missing=0
|
|
local bucket,i,record
|
|
for key,bucket in pairs(store.historyByChannel) do
|
|
if type(bucket)=="table" then
|
|
for i=1,table.getn(bucket) do
|
|
record=bucket[i]
|
|
if type(record)=="table" and type(record.sequence)~="number" then
|
|
missing=missing+1
|
|
record.sequence=-100000000+missing
|
|
end
|
|
end
|
|
end
|
|
end
|
|
MCHistory.Trim(store,defaultLimit)
|
|
end
|
|
|
|
function MCHistory.Add(store, defaultLimit, record)
|
|
local key=MCHistory.Key(record)
|
|
local bucket=store.historyByChannel[key]
|
|
if not bucket then
|
|
bucket={}
|
|
store.historyByChannel[key]=bucket
|
|
end
|
|
if not store.historyHeads then store.historyHeads={} end
|
|
local limit=MCHistory.Limit(store,defaultLimit,key)
|
|
local n=table.getn(bucket)
|
|
local head=store.historyHeads[key] or 1
|
|
if n<limit then
|
|
-- A changed limit can leave a rotated, partly-filled bucket.
|
|
-- Normalize it once before appending, not on every incoming message.
|
|
if head~=1 then
|
|
normalize(store,defaultLimit,key,bucket)
|
|
bucket=store.historyByChannel[key]
|
|
end
|
|
table.insert(bucket,record)
|
|
else
|
|
bucket[head]=record
|
|
store.historyHeads[key]=math.mod(head,limit)+1
|
|
end
|
|
invalidate()
|
|
end
|
|
|
|
function MCHistory.SetLimit(store,defaultLimit,key,value)
|
|
if type(key)~="string" or type(value)~="number" then return end
|
|
store.historyLimits[key]=math.floor(value)
|
|
MCHistory.Trim(store,defaultLimit)
|
|
end
|
|
|
|
function MCHistory.ResetLimit(store,defaultLimit,key)
|
|
if type(key)~="string" then return end
|
|
store.historyLimits[key]=nil
|
|
MCHistory.Trim(store,defaultLimit)
|
|
end
|
|
|
|
-- Clear every retained channel (including Combat Log and legacy imports).
|
|
-- Preserve limits, channel settings, tabs, and sequence counter.
|
|
function MCHistory.Clear(store)
|
|
store.historyByChannel={}
|
|
store.historyHeads={}
|
|
-- Never re-import pre-v0.8.5 arrays after a clear or a relog.
|
|
store.entries=nil
|
|
store.combatEntries=nil
|
|
store.historyMigrated085=true
|
|
invalidate()
|
|
end
|
|
|
|
function MCHistory.Count(store,key)
|
|
local count=0
|
|
local name,bucket
|
|
for name,bucket in pairs(store.historyByChannel or {}) do
|
|
if key==nil or name==key or (key=="CHANNEL" and string.sub(name,1,8)=="CHANNEL:") then
|
|
count=count+table.getn(bucket)
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
-- Compatibility method for callers requiring a merged list. History UI no
|
|
-- longer uses this; it fetches small windows via MCHistory.Window instead.
|
|
function MCHistory.Records(store,combat)
|
|
local cached=combat and cachedCombat or cachedChat
|
|
if cached then return cached end
|
|
local list={}
|
|
local key,bucket,i,head
|
|
for key,bucket in pairs(store.historyByChannel or {}) do
|
|
if (key=="COMBAT")==combat then
|
|
head=(store.historyHeads or {})[key] or 1
|
|
for i=1,table.getn(bucket) do
|
|
table.insert(list,getRecord(bucket,head,i))
|
|
end
|
|
end
|
|
end
|
|
table.sort(list,function(a,b) return (a.sequence or 0)<(b.sequence or 0) end)
|
|
if combat then cachedCombat=list else cachedChat=list end
|
|
return list
|
|
end
|
|
|
|
-- Fetch at most (limit + 1) matching records without merging/sorting the
|
|
-- entire saved history. Each channel bucket is sequence-ordered; use binary
|
|
-- search to find the starting cursor, then a small k-way merge.
|
|
-- direction: "older" (latest first) or "newer" (earliest first).
|
|
-- pivot: exclusive message sequence boundary; nil means newest/oldest.
|
|
-- acceptBucket(key) skips disabled channels before any record work.
|
|
-- acceptRecord(record) applies tab filters and Combat Log mini-tab selection.
|
|
-- Returns a chronological page plus whether more matches exist in direction.
|
|
function MCHistory.Window(store, direction, pivot, limit, acceptBucket, acceptRecord)
|
|
local cursors={}
|
|
local key,bucket,n,head,lo,hi,mid,idx,record,sequence
|
|
local older=direction=="older"
|
|
for key,bucket in pairs(store.historyByChannel or {}) do
|
|
if type(bucket)=="table" and table.getn(bucket)>0 and
|
|
(not acceptBucket or acceptBucket(key)) then
|
|
n=table.getn(bucket)
|
|
head=(store.historyHeads or {})[key] or 1
|
|
if pivot==nil then
|
|
idx=older and n or 1
|
|
else
|
|
lo=1;hi=n
|
|
if older then
|
|
idx=0
|
|
while lo<=hi do
|
|
mid=math.floor((lo+hi)/2)
|
|
record=getRecord(bucket,head,mid)
|
|
sequence=record and record.sequence or 0
|
|
if sequence<pivot then idx=mid;lo=mid+1
|
|
else hi=mid-1 end
|
|
end
|
|
else
|
|
idx=n+1
|
|
while lo<=hi do
|
|
mid=math.floor((lo+hi)/2)
|
|
record=getRecord(bucket,head,mid)
|
|
sequence=record and record.sequence or 0
|
|
if sequence>pivot then idx=mid;hi=mid-1
|
|
else lo=mid+1 end
|
|
end
|
|
end
|
|
end
|
|
if idx>=1 and idx<=n then
|
|
table.insert(cursors,{bucket=bucket,head=head,index=idx,count=n})
|
|
end
|
|
end
|
|
end
|
|
local gathered={}
|
|
local hasMore=false
|
|
local i,c,best,bestSeq,candidate,cseq
|
|
while true do
|
|
best=nil;bestSeq=nil
|
|
for i=1,table.getn(cursors) do
|
|
c=cursors[i]
|
|
if c.index>=1 and c.index<=c.count then
|
|
candidate=getRecord(c.bucket,c.head,c.index)
|
|
cseq=candidate and candidate.sequence or 0
|
|
if bestSeq==nil or (older and cseq>bestSeq) or (not older and cseq<bestSeq) then
|
|
best=i;bestSeq=cseq
|
|
end
|
|
end
|
|
end
|
|
if not best then break end
|
|
c=cursors[best]
|
|
candidate=getRecord(c.bucket,c.head,c.index)
|
|
if older then c.index=c.index-1 else c.index=c.index+1 end
|
|
if not acceptRecord or acceptRecord(candidate) then
|
|
if table.getn(gathered)>=limit then hasMore=true;break end
|
|
table.insert(gathered,candidate)
|
|
end
|
|
end
|
|
if older then
|
|
local chronological={}
|
|
for i=table.getn(gathered),1,-1 do table.insert(chronological,gathered[i]) end
|
|
return chronological,hasMore
|
|
end
|
|
return gathered,hasMore
|
|
end
|