XP System for Eggdrop (Multi‑Channel) – Simple DAT version

########################################################################
#  XP System for Eggdrop (Multi‑Channel) – Simple DAT version
#  Author: YnM  |  Edited: Markus ([email protected])
#  Copyright © 2024‑2025  YnM – Minden jog fenntartva.
########################################################################

###############################  Beállítások  ##########################

set xp_db            "YnM/tmp/xp.dat"
set xp_backup_dir    "YnM/tmp/backups"
set xp_cooldown      5            ;# másodperc
set xp_backup_interval 21600       ;# 6 óra
set ::xp_excluded_nicks { "YnM-Ai" "YnM-Bridge" }
set ::xp_channels { "#Magyar" "#YnM" "#Help" }

array set ::ranks {
    0     "Újonc"
    100   "Haladó"
    500   "Veterán"
    1000  "Mester"
    10000 "Legenda"
    100000 "10‑zesek"
}

############################  Segédfüggvények  #########################

proc xp_log {level msg} { putlog "XP $level: $msg" }

# --- Könyvtár‑ellenőrzés / létrehozás ---
proc ensure_dirs {} {
    set dirs [list "YnM/tmp" "YnM/tmp/backups"]
    foreach dir $dirs {
        if {![file isdirectory $dir]} {
            if {[catch {file mkdir $dir} err]} {
                xp_log ERROR "Nem sikerült létrehozni: $dir – $err"
                return 0
            } else {
                xp_log INFO  "Létrehozva: $dir"
            }
        }
        if {![file writable $dir]} {
            xp_log ERROR "Nincs írási jog: $dir"
            return 0
        }
    }
    return 1
}

# --- DAT fájl betöltése dictbe ---
proc load_db {filename} {
    set data [dict create]
    if {![file exists $filename]} { return $data }

    if {[catch {set fh [open $filename r]} err]} {
        xp_log ERROR "Nem olvasható a fájl ($filename): $err"
        return $data
    }
    while {[gets $fh line] >= 0} {
        if {[regexp {^(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)$} $line -> chan nick xp lg tm]} {
            dict set data $chan $nick \
                [dict create xp $xp last_xp_gain $lg total_messages $tm]
        } elseif {$line ne ""} {
            xp_log WARN "Hibás sor kihagyva: $line"
        }
    }
    close $fh
    return $data
}

# --- Dict mentése DAT fájlba (hibatűrő) ---
proc save_db {filename data} {
    if {[catch {set fh [open $filename w]} err]} {
        xp_log ERROR "Nem írható a fájl ($filename): $err"
        return 0
    }
    dict for {chan users} $data {
        dict for {nick ud} $users {
            if {[catch {set xp  [dict get $ud xp]}]}              { set xp 0 }
            if {[catch {set lg  [dict get $ud last_xp_gain]}]}    { set lg 0 }
            if {[catch {set tm  [dict get $ud total_messages]}]}  { set tm 0 }
            puts $fh "$chan $nick $xp $lg $tm"
        }
    }
    close $fh
    return 1
}

# --- Rang meghatározása ---
proc get_rank {xp} {
    set thresholds [lsort -integer -decreasing [array names ::ranks]]
    foreach thr $thresholds {
        if {$xp >= $thr} { return $::ranks($thr) }
    }
    return "Ismeretlen"
}

#############################  XP kezelés  #############################

# XP lekérdezése
proc get_xp {nick chan} {
    set db [load_db $::xp_db]
    if {[dict exists $db $chan $nick xp]} {
        return [dict get $db $chan $nick xp]
    }
    return 0
}

# XP hozzáadása (cooldown‑nal)
proc add_xp {nick chan amount} {
    set now [clock seconds]
    set db  [load_db $::xp_db]

    if {[dict exists $db $chan $nick]} {
        set ud [dict get $db $chan $nick]
        if {[dict exists $ud last_xp_gain] &&
            ($now - [dict get $ud last_xp_gain]) < $::xp_cooldown} {
            return "cooldown"
        }
        dict incr ud xp $amount
        dict set ud total_messages [expr {[dict get $ud total_messages] + 1}]
    } else {
        set ud [dict create xp $amount total_messages 1]
    }
    dict set ud last_xp_gain $now
    dict set db $chan $nick $ud

    if {![save_db $::xp_db $db]} {
        return ""
    }
    return [dict get $ud xp]
}

###########################  IRC bind függvények  ######################

# Üzenet alapján XP
bind pubm - * xp_gain
proc xp_gain {nick uhost hand chan text} {
    if {[lsearch -exact $::xp_channels $chan] == -1} return
    if {[lsearch -exact $::xp_excluded_nicks $nick] != -1} return
    if {[string length $text] < 3 || [string index $text 0] eq "!"} return

    set len [string length $text]
	set words [llength [split $text]]
	if {$words == 1} {
		set add 1
	} elseif {$len <= 10} {
		set add 5
	} elseif {$len <= 20} {
		set add 10
	} else {
		set add 10
	}

    set oldxp [get_xp $nick $chan]
    set oldrk [get_rank $oldxp]
    set newxp [add_xp $nick $chan $add]

    if {$newxp eq "cooldown" || $newxp eq ""} return

    set newrk [get_rank $newxp]
    if {$newrk ne $oldrk} {
        putserv "PRIVMSG $chan :$nick rangot lépett: $newrk ($newxp XP)"
    }
}

# !xp
bind pub - !xp xp_cmd
proc xp_cmd {nick uhost hand chan text} {
    if {[lsearch -exact $::xp_channels $chan] == -1} return
    set tgt [string trim $text]
    if {$tgt eq ""} { set tgt $nick }

    set db [load_db $::xp_db]
    set outlist {}

    dict for {c users} $db {
        if {[dict exists $users $tgt]} {
            set xp [dict get $users $tgt xp]
            lappend outlist "$c: $xp XP"
        }
    }

    if {[llength $outlist] == 0} {
        putserv "PRIVMSG $chan :$tgt nem rendelkezik XP-vel egyik csatornán sem."
        return
    }

    putserv "PRIVMSG $chan :$tgt XP értékei: [join $outlist {, }]"
}




bind pub n !xpreset resetxp_cmd
proc resetxp_cmd {nick uhost hand chan text} {
    if {![matchattr $hand n]} return
    set tgt [string trim $text]
    if {$tgt eq ""} { putserv "PRIVMSG $chan :Használat: !resetxp NICK"; return }
    set db [load_db $::xp_db]
    dict set db $chan $tgt [dict create xp 0 last_xp_gain 0 total_messages 0]
    save_db $::xp_db $db
    putserv "PRIVMSG $chan :$tgt XP nullázva."
}

bind pub n !xpadd addxp_cmd
proc addxp_cmd {nick uhost hand chan text} {
    if {![matchattr $hand n]} return
    set args [split $text]
    
    if {[llength $args] == 2} {
        # formátum: NICK Mennyiség
        set tgt [lindex $args 0]
        set amount [lindex $args 1]
        set target_chan $chan
    } elseif {[llength $args] == 3} {
        # formátum: CSATORNA NICK Mennyiség
        set target_chan [lindex $args 0]
        set tgt [lindex $args 1]
        set amount [lindex $args 2]
        # opcionálisan ellenőrizheted, hogy a target_chan benne legyen az engedélyezett csatornákban
        if {[lsearch -exact $::xp_channels $target_chan] == -1} {
            putserv "PRIVMSG $chan :Érvénytelen csatorna: $target_chan"
            return
        }
    } else {
        putserv "PRIVMSG $chan :Használat: !addxp [#csatorna] NICK Mennyiség"
        return
    }
    
    if {![string is integer -strict $amount]} {
        putserv "PRIVMSG $chan :Hibás mennyiség: $amount"
        return
    }
    
    set newxp [add_xp $tgt $target_chan $amount]
    if {$newxp ne "" && $newxp ne "cooldown"} {
        putserv "PRIVMSG $chan :$tgt új XP: $newxp ($target_chan)"
    }
}


# !xptop
bind pub - !xptop xptop_cmd
proc xptop_cmd {nick uhost hand chan text} {
    if {[lsearch -exact $::xp_channels $chan] == -1} return
    set db [load_db $::xp_db]
    if {![dict exists $db $chan]} {
        putserv "PRIVMSG $chan :Még nincs XP adat ezen a csatornán."
        return
    }
    set top {}
    dict for {u d} [dict get $db $chan] {
        lappend top [list $u [dict get $d xp]]
    }
    set top [lsort -integer -decreasing -index 1 $top]
    set top [lrange $top 0 9]
    set out [list]
    set r 1
    foreach item $top {
        lappend out "$r. [lindex $item 0] ([lindex $item 1] XP)"
        incr r
    }
    putserv "PRIVMSG $chan :Top 10: [join $out {, }]"
}

# !globaltop
bind pub - !xpglobal xpglobal_cmd
proc xpglobal_cmd {nick uhost hand chan text} {
    if {[lsearch -exact $::xp_channels $chan] == -1} return
    set db [load_db $::xp_db]

    # Létrehozunk egy dictet user => {csatorna => xp}
    set users_dict [dict create]

    dict for {c users} $db {
        dict for {u d} $users {
            set xp [dict get $d xp]
            if {[dict exists $users_dict $u]} {
                dict set users_dict $u $c $xp
            } else {
                dict set users_dict $u [dict create $c $xp]
            }
        }
    }

    if {[dict size $users_dict] == 0} {
        putserv "PRIVMSG $chan :Nincs XP adat az adatbázisban."
        return
    }

    # Kimenet összeállítása felhasználónként
    set outlist {}
    dict for {user chans} $users_dict {
        set parts {}
        dict for {ch xp} $chans {
            lappend parts "$ch: $xp XP"
        }
        lappend outlist "$user XP értékei: [join $parts {, }]"
    }

    # Üzenetek küldése (több sor is lehet)
    foreach line $outlist {
        putserv "PRIVMSG $chan :$line"
    }
}



bind pub n !xpdel xpdel_cmd

proc xpdel_cmd {nick uhost hand chan text} {
    if {![matchattr $hand n]} return
    set tgt [string trim $text]
    if {$tgt eq ""} {
        putserv "PRIVMSG $chan :Használat: !xpdel Nick"
        return
    }

    set db [load_db $::xp_db]

    set found 0
    dict for {c users} $db {
        if {[dict exists $users $tgt]} {
            dict unset users $tgt
            dict set db $c $users
            set found 1
        }
    }

    if {!$found} {
        putserv "PRIVMSG $chan :$tgt nem szerepel az XP adatbázisban."
        return
    }

    if {[save_db $::xp_db $db]} {
        putserv "PRIVMSG $chan :$tgt XP adatai minden csatornából törölve lettek."
    } else {
        putserv "PRIVMSG $chan :Hiba történt az XP adatbázis mentésekor."
    }
}
########################  Biztonsági mentés timer  #####################

proc backup_db {} {
    set ts [clock format [clock seconds] -format "%Y%m%d_%H%M%S"]
    set dest "$::xp_backup_dir/xp_backup_$ts.dat"

    if {[file exists $::xp_db]} {
        catch { file copy -force $::xp_db $dest }
        xp_log INFO "Backup készült: $dest"
    }

    # --- Régi backup fájlok törlése (max 5 maradjon meg) ---
    set backup_files [glob -nocomplain -directory $::xp_backup_dir xp_backup_*.dat]
    set sorted [lsort -increasing $backup_files]
    set excess_count [expr {[llength $sorted] - 5}]
    if {$excess_count > 0} {
        foreach f [lrange $sorted 0 [expr {$excess_count - 1}]] {
            catch { file delete -force $f }
            xp_log INFO "Régi backup törölve: $f"
        }
    }

    timer $::xp_backup_interval backup_db
}
###############################  Indítás  ##############################

if {![ensure_dirs]} { xp_log ERROR "Könyvtárhiba, kilépés."; return }

if {[file exists $xp_db]} {
    xp_log INFO "XP adatbázis betöltve: $xp_db"
} else {
    save_db $xp_db [dict create]
    xp_log INFO "Új üres adatbázis létrehozva: $xp_db"
}

backup_db
xp_log INFO "Multi‑channel XP system (DAT) loaded"