(in-package :lispostory) (define-data-source spawn) (defparameter *chronostory-spawns-url-format* "https://docs.google.com/spreadsheets/d/e/2PACX-1vSIUj-72ADgwMqShxt4Dn7OP7dBN54l0wda1IPwlIVTZUN_ZtTlRx5DDidr43VXv2HYQ5RNqccLbbGS/pub?gid=~a&output=csv&single=true") (defparameter *chronostory-spawn-gids* (alist :maple-island 415330053 :lith-harbor 486394009 :kerning-city 0 :henesys 898655980 :perion 1754196543 :ellinia 508846815 :nautilus 714441637 :sleepywood 615729202 :orbis 350970245 :el-nath 1897049096 :ludibrium 1290087592)) (defun chronostory-spawn-url (gid) (format nil *chronostory-spawns-url-format* gid)) (defun find-first-empty-field (line) "Upon receiving the csvs from google sheets, we need to find the first 'empty' header column. That column is the delimiter between two separate csvs." (loop for i from 0 below (- (length line) 1) with comma-count = 0 when (char= (char line i) #\,) do (incf comma-count) when (and (char= (char line i) #\,) (char= (char line (+ i 1)) #\,)) return comma-count)) (defun find-char-index-of-field (line field) "Could be an off by one here?" (loop for i from 0 below (length line) with comma-count = 0 when (char= (char line i) #\,) do (incf comma-count) when (= comma-count field) return i)) (defun second-half (line field) (let* ((index (find-char-index-of-field line field)) (second-half (subseq line (+ 2 index)))) second-half)) (defun string-to-number-p (str) (handler-case (progn (parse-integer str) t) (parse-error () nil))) (defun csv-to-hash-tables (csv) (let* ((keys (mapcar #'string-to-hash-table-key (car csv))) (result (make-array 0 :adjustable t :fill-pointer 0))) (loop for row in csv with mapname = "" for hm = (pairhash keys row (dict)) when (let ((name (gethash "Monster" hm))) (and (string-not-equal "" name) (string-not-equal "-" name) (string-not-equal "Monster" name))) do (progn (if (and (string-not-equal mapname "") (string-equal (gethash "MapName" hm) "")) (setf (gethash "MapName" hm) mapname) (setf mapname (gethash "MapName" hm))) (vector-push-extend hm result))) result)) (defun parse-sheet (sheet) (let* ((lines (lines sheet)) (field (find-first-empty-field (car lines))) (first-half (loop for line in lines when (not (uiop:string-prefix-p ",,," line)) collect (let ((index (find-char-index-of-field line field))) (subseq line 0 index)))) (second-half (loop for line in lines for second-half = (second-half line field) when (not (uiop:string-prefix-p ",,," second-half)) collect second-half)) (joined (string-join (concatenate 'list first-half second-half) #\Newline))) (csv-to-hash-tables (cl-csv:read-csv joined)))) (defmethod refresh ((ds spawn-data-source)) (let ((spawn-data (pairhash (mapcar #'car *chronostory-spawn-gids*) (mapcar (lambda (pair) (let* ((gid (cdr pair)) (data (drakma:http-request (chronostory-spawn-url gid)))) (uiop:println (concat "Requesting data for " (symbol-name (car pair)))) (parse-sheet data))) *chronostory-spawn-gids*)))) (setf (cache ds) spawn-data) (write-string-into-file (shasht:write-json spawn-data nil) (file-path ds) :if-exists :overwrite :if-does-not-exist :create) spawn-data)) (defun get-spawns-for-mob (mob-name &optional (ds (make-spawn-data-source))) (loop :for areas :in (hash-table-values (data ds)) :append (loop :for spawn :across areas :when (string-equal (@ spawn "Monster") mob-name) collect spawn))) (comment (defvar spawn-ds (make-spawn-data-source)) (data (make-spawn-data-source)) (mapcar (partial #'gethash "MapName") (get-spawns-for-mob "Snail")))