| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- (in-package :cl-user)
- (defpackage chatikbot.plugins.q3a
- (:use :cl :chatikbot.common :alexandria))
- (in-package :chatikbot.plugins.q3a)
- (defsetting *server-log* "/home/gamer/.q3a/baseq3/server.log" "Path to Q3Arena server log file to watch")
- (defvar *watcher* nil "Log watcher thread if set")
- (defvar *lock-game* (bordeaux-threads:make-recursive-lock "Q3A lock"))
- (defun watch-file (filespec thunk &optional (start-at :end))
- "This will block forever, waiting for new data"
- (with-open-file (s filespec)
- (file-position s start-at)
- (loop
- with last-pos = (file-position s)
- for line = (read-line s nil nil)
- when line do (progn
- (funcall thunk line)
- (setf last-pos (file-position s)))
- else do (progn
- (sleep 0.1)
- (when (> last-pos (file-length s))
- (file-position s :start))))))
- (defun parse-bslash-list (str)
- (loop for (key val) on
- (loop for pos = 0 then (1+ next-pos)
- for next-pos = (position #\\ str :start pos)
- collect (subseq str pos next-pos)
- while next-pos) by 'cddr
- collect (cons key val)))
- (defstruct game info players last-time is-active)
- (defstruct player name info kills began)
- (defstruct kill whom how)
- (defvar *game* (make-game) "Currently active game")
- (defun max-players (&optional (game *game*))
- (length (game-players game)))
- (defun map-name (&optional (game *game*))
- (agets (game-info game) "mapname"))
- (defun suicidep (player kill)
- (or (null (kill-whom kill))
- (equal (kill-whom kill) player)))
- (defun player-score (player)
- (loop for kill in (player-kills player)
- when (suicidep player kill) sum -1 else sum 1))
- (defun get-rankings (&optional (game *game*))
- (sort (loop for player across (game-players game)
- when (player-began player)
- collect (cons (player-score player) player))
- #'> :key #'car))
- (defun game-status (&optional (game *game*))
- (let ((ranks (get-rankings)))
- (if ranks
- (format nil (if (game-is-active game)
- "[Q3] Match *~A* is on for ~A~%~{~A~^~%~}"
- "[Q3] Match *~A* has ended after ~A~%~{~A~^~%~}")
- (map-name) (game-last-time *game*)
- (loop for (score . player) in ranks
- collect (format nil "*~A* ~A" score (player-name player))))
- "[Q3] War awaits")))
- (defun say (fmt &rest args)
- (let ((words (apply #'format nil fmt args)))
- (write words) (terpri)
- (dolist (*chat-id* (lists-get :q3a))
- (bot-send-message words))))
- (defun weapon (kill)
- (kill-how kill))
- (defun on-init ())
- (defun on-begin (player)
- (unless (player-began player)
- (say "[Q3] ~A joined the server on ~A" (player-name player) (map-name))))
- ;; (defun on-kill (player kill)
- ;; (let* ((name (player-name player))
- ;; (who (kill-whom kill)))
- ;; (if (suicidep player kill)
- ;; (say "[Q3] ~A suicides with score ~A" name (player-score player))
- ;; (say "[Q3] ~A frags ~A with score ~A" name (player-name who) (player-score player)))))
- (defun on-kill (player kill)
- (declare (ignorable player kill)))
- (defun on-disconnect (player)
- (when (player-began player)
- (say "[Q3] ~A has left the match with ~A frags" (player-name player) (player-score player))))
- (defun on-exit ()
- (say (game-status)))
- (defun process-log (line)
- (bordeaux-threads:with-recursive-lock-held (*lock-game*)
- (let* ((time (subseq line 0 7))
- (sep (position #\: line :start 7))
- (action (keyify (subseq line 7 sep)))
- (args (when (and sep (> (length line) (+ 2 sep))) (subseq line (+ 2 sep))))
- (client (when args (parse-integer args :junk-allowed t)))
- (player (when (and client (<= 0 client (1- (max-players)))) (elt (game-players *game*) client))))
- (setf (game-last-time *game*) (trim-nil time))
- (case action
- (:initgame (with-slots (info players is-active) *game*
- (setf info (parse-bslash-list (subseq args 1))
- is-active t
- players (or players
- (let ((count (parse-integer (agets info "sv_maxclients"))))
- (make-array count :element-type 'player
- :initial-contents (loop for i below count collect (make-player))))))
- (on-init)))
- (:clientuserinfochanged (with-slots (info name) player
- (setf info (parse-bslash-list
- (subseq args (1+ (position #\Space args))))
- name (agets info "n"))))
- (:clientbegin (progn
- (on-begin player)
- (setf (player-began player) t)))
- (:clientdisconnect (progn
- (on-disconnect player)
- (setf (player-began player) nil)))
- (:kill (destructuring-bind (a1 a2 how)
- (mapcar 'parse-integer (split-sequence:split-sequence
- #\Space (subseq args 0 (position #\: args))))
- (let* ((killee (when (<= a1 (max-players))
- (elt (game-players *game*) a2)))
- (killer (elt (game-players *game*) (if killee a1 a2)))
- (kill (make-kill :whom killee :how how)))
- (push kill (player-kills killer))
- (on-kill killer kill))))
- (:exit (progn
- (setf (game-is-active *game*) nil)
- (on-exit)))
- (:shutdowngame (loop for player across (game-players *game*)
- do (setf (player-kills player) nil))))))
- (values))
- (defun ensure-watcher ()
- (bt:with-recursive-lock-held (*lock-game*)
- (if (lists-get :q3a)
- (unless *watcher*
- (setf *watcher* (bt:make-thread (lambda () (watch-file *server-log* #'process-log)) :name "q3a watch")))
- (when *watcher*
- (bt:destroy-thread *watcher*)
- (setf *watcher* nil))))
- (values))
- (defun handle-set-watch (enable)
- (lists-set-entry :q3a *chat-id* enable)
- (ensure-watcher)
- (bot-send-message (if enable "[Q3] Шлём войну!" "[Q3] Миру-мир!")))
- (defun handle-active ()
- (bot-send-message (game-status)))
- (def-message-cmd-handler handle-cmd-q3a (:q3a)
- (cond
- ((= 1 (length *args*))
- (handle-set-watch (equal "on" (car *args*))))
- (:otherwise (handle-active))))
- (add-hook :starting #'ensure-watcher)
|