Crossfire Client, Trunk
cfmaplog.py
Go to the documentation of this file.
1 #!/bin/python
2 #
3 license = '''
4 cfmaplog.py - Crossfire GTK Client plug-in to track per-character map visits.
5 Copyright (C) 2025, "Kevin R. Bulgrien" <kbulgrien@att.net>
6 
7 This program is free software: you can redistribute it and/or modify it under
8 the terms of the GNU General Public License as published by the Free Software
9 Foundation, either version 3 of the License, or (at your option) any later
10 version.
11 
12 This program is distributed in the hope that it will be useful, but WITHOUT
13 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15 details.
16 
17 You should have received a copy of the GNU General Public License along with
18 this program. If not, see <https://www.gnu.org/licenses/>.
19 '''
20 
21 # os.path.expanduser()
22 import io
23 
24 # os.path.expanduser()
25 import os
26 
27 # regex
28 import re
29 
30 # database
31 import sqlite3
32 
33 # stderr,stdin,stdout
34 import sys
35 
36 # time.sleep(), time.strftime()
37 import time
38 
39 debug = False
40 
41 # Functions ##################################################################
42 
43 # Send something to the "server". If a newline should go, and it usually
44 # should, pass one at the end of the text. This script may build output
45 # incrementally, so do not add one here.
46 #
47 def client_send(text):
48  sys.stdout.write(f"{text}{os.linesep}")
49  sys.stdout.flush()
50  return
51 
52 # Send something to the "user" via stderr. For these to be seen, the client
53 # is started in a console.
54 #
55 def console_send(text):
56  sys.stderr.write(f"{text}{os.linesep}")
57  sys.stderr.flush()
58  return
59 
60 # Send something to facilitate debugging via stderr. For these to be seen,
61 # the client is started in a console.
62 #
63 def debug_send(text):
64  if debug:
65  console_send(f"{text}{os.linesep}")
66  return
67 
68 # Send something to the "player" by way of the Messages pane. If a newline
69 # should go, and it usually should, pass one at the end of the text. This
70 # script may build output incrementally, so do not add one here.
71 #
72 # IMPORTANT: It appears that the integer-based color is currently broken
73 # in the client; text markup is required to set text attributes.
74 #
75 # See Also: client/common/shared/newclient.h
76 #
77 NDI_BLACK = 0
78 NDI_WHITE = 1
79 NDI_NAVY = 2
80 NDI_RED = 3
81 NDI_ORANGE = 4
82 NDI_BLUE = 5 # Actually, it is Dodger Blue
83 NDI_DK_ORANGE = 6 # DarkOrange2
84 NDI_GREEN = 7 # SeaGreen
85 NDI_LT_GREEN = 8 # DarkSeaGreen, which is actually paler
86  # than seagreen - also background color.
87 NDI_GREY = 9
88 NDI_BROWN = 10 # Sienna.
89 NDI_GOLD = 11
90 NDI_TAN = 12 # Khaki.
91 NDI_MAX_COLOR = 12 # Last value in.
92 
93 NDI_COLOR_MASK = 0xff # Gives lots of room for expansion - we are
94  # using an int anyways, so we have the
95  # space to still do all the flags.
96 
97 NDI_UNIQUE = 0x100 # Print immediately, don't buffer.
98 NDI_ALL = 0x200 # Inform all players of this message.
99 NDI_ALL_DMS = 0x400 # Inform all logged in DMs. Used in case of
100 
101 # See Also: client/gtk-v2/src/info.c
102 #
103 # The following markup strings are supported:
104 #
105 # [b] ... [/b] bold
106 # [i] ... [/i] italic
107 # [ul] ... [/ul] underline
108 # [print] font_style_names[0] ???
109 # [arcane] font_style_names[1]
110 # [strange] font_style_names[2]
111 # [fixed] font_style_names[3]
112 # [hand] font_style_names[4]
113 # [color=x] ... [/color] where x are the quoted color names below.
114 #
115 # font_style_names[] are determined by the selected theme. The default
116 # installed theme files are in client/gtk-v2/themes and are "Standard"
117 # and Black. Both themes use the same set of fonts by default.
118 #
119 # font_style_names[0] = <system/theme default> ???
120 # font_style_names[1] = "URW Chancery Lk"
121 # font_style_names[2] = "Sans Italic"
122 # font_style_names[3] = "Luxi Mono"
123 # font_style_names[4] = "Century Schoolbook L Italic"
124 #
125 # Color names set by the user in the gtkrc file. */
126 # static const char *const usercolorname[NUM_COLORS] = {
127 # "black", /* 0 */
128 # "white", /* 1 */
129 # "darkblue", /* 2 */
130 # "red", /* 3 */
131 # "orange", /* 4 */
132 # "lightblue", /* 5 */
133 # "darkorange", /* 6 */
134 # "green", /* 7 */
135 # "darkgreen", /* 8 *//* Used for window background color */
136 # "grey", /* 9 */
137 # "brown", /* 10 */
138 # "yellow", /* 11 */
139 # "tan" /* 12 */
140 #
141 def player_send(text):
142  lines = text.split(os.linesep)
143  for line in lines:
144  line = line.rstrip(os.linesep)
145  client_send(f"draw 0 [color=darkorange]{codefile}: {line}")
146  debug_send(f"{codefile}: {line}")
147  return
148 
150  lines = text.split(os.linesep)
151  for line in lines:
152  line = line.rstrip(os.linesep)
153  client_send(f"draw 0 {line}")
154  debug_send(f"{line}")
155  return
156 
158  #
159  # Does a visit log exist for this map and player? This may fail if a player
160  # hasn't visited the map before when the logger was active. If not found,
161  # a map cannot have been marked completed.
162  #
163  if map_id and player_id:
164  cursor.execute('''
165  SELECT COMPLETED, COMPLETED_DATE FROM visit
166  WHERE MAP_ID = ? AND PLAYER_ID = ?
167  ''', ( map_id, player_id ))
168  query = cursor.fetchone()
169  if query != None:
170  completed = query[0]
171  completed_date = query[1]
172  if completed:
173  player_send(f"You marked this area completed {completed} times.")
174  player_send(f"The most recent completion was: {completed_date}.")
175 
176 # Begin ######################################################################
177 
178 try:
179  server_name = os.environ['CF_SERVER_NAME']
180  player_name = os.environ['CF_PLAYER_NAME']
181 except:
182  console_send(f"{license}")
183  console_send(f"This plug-in is not meant to run as a standalone program.")
184  sys.exit(0)
185 
186 # Sometimes its hard to pick out where the beginning of the run is located
187 # when stopping and starting the script during development so output this.
188 #
189 debug_send("" + \
190  "-------------------------------------------------------------------------")
191 
192 # sqlite3 initialization #####################################################
193 #
194 codefile = re.compile("[.][^.]+\Z").sub("", __file__)
195 datafile = codefile + ".db"
196 codefile = re.compile("^.+\/").sub("", codefile)
197 debug_send(f"{datafile}")
198 codepath = __file__
199 
200 try:
201  dbConn = sqlite3.connect(f"{datafile}")
202 except:
203  console_send(f"{e}")
204  player_send(f"{datafile}")
205  player_send(f"[color=red]sqlite3.connect error[/color]")
206  player_send(f"exiting...")
207  sys.exit(0)
208 
209 cursor = dbConn.cursor()
210 
211 # server initialization ######################################################
212 #
213 servers = 0
214 server_id = 0
215 
216 debug_send(f"CF_SERVER_NAME: {server_name}")
217 
218 try:
219  cursor.execute('''
220  CREATE TABLE IF NOT EXISTS server (
221  SERVER_ID INTEGER PRIMARY KEY NOT NULL,
222  SERVER_NAME TEXT UNIQUE NOT NULL
223  )
224  ''')
225 except:
226  console_send(f"{e}")
227  player_send(f"{datafile}")
228  player_send(f"[color=red]CREATE TABLE server error[/color]")
229  player_send(f"exiting...")
230  sys.exit(0)
231 
232 try:
233  cursor.execute('''
234  SELECT COUNT(*) FROM server
235  ''')
236 except:
237  console_send(f"{e}")
238  player_send(f"{datafile}")
239  player_send(f"[color=red]SELECT FROM server error[/color]")
240  player_send(f"exiting...")
241  sys.exit(0)
242 
243 query = cursor.fetchone()
244 servers = query[0]
245 
246 # Get or assign a server_id for the current server.
247 #
248 try:
249  cursor.execute('''
250  SELECT SERVER_ID FROM server
251  WHERE SERVER_NAME = ?
252  ''', ( server_name, ))
253 except:
254  console_send(f"{e}")
255  player_send(f"{datafile}")
256  player_send(f"[color=red]SELECT FROM server error[/color]")
257  player_send(f"exiting...")
258 
259 query = cursor.fetchone()
260 if query == None:
261  servers = servers + 1
262  server_id = servers
263  try:
264  cursor.execute('''
265  INSERT INTO server
266  ( SERVER_ID, SERVER_NAME )
267  VALUES ( ?, ? )
268  ''', (servers, server_name))
269  except:
270  console_send(f"{e}")
271  player_send(f"{datafile}")
272  player_send(f"[color=red]INSERT INTO server error[/color]")
273  player_send(f"exiting...")
274  sys.exit(0)
275 
276  dbConn.commit()
277 else:
278  server_id = query[0]
279 
280 debug_send(f"server_name (server_id): {server_name} ({server_id})")
281 
282 # player initialization ######################################################
283 #
284 # request player
285 # request player 1481 Player: Brayagorn the human
286 
287 players = 0
288 player_code = 0
289 player_title = ''
290 player_seen = time.strftime("%Y/%m/%d %H:%M")
291 
292 debug_send(f"CF_PLAYER_NAME: {player_name}")
293 
294 regx_rqst_player = "^request\splayer\s"
295 regc_rqst_player = re.compile(regx_rqst_player)
296 regx_rqst_player_id = "(\d+)\s+"
297 regc_rqst_player_id = re.compile(regx_rqst_player_id)
298 regx_rqst_player_strt = regx_rqst_player + regx_rqst_player_id
299 regc_rqst_player_strt = re.compile(regx_rqst_player_strt)
300 regx_rqst_player_name = "Player:\s+(\w+)\s+(.+)"
301 regc_rqst_player_name = re.compile(regx_rqst_player_name)
302 regx_rqst_player_data = regx_rqst_player_id + regx_rqst_player_name
303 regc_rqst_player_data = re.compile(regx_rqst_player_data)
304 
305 try:
306  cursor.execute('''
307  CREATE TABLE IF NOT EXISTS player (
308  PLAYER_ID INTEGER PRIMARY KEY NOT NULL,
309  SERVER_ID INTEGER NOT NULL,
310  PLAYER_NAME TEXT NOT NULL,
311  PLAYER_TITLE TEXT NOT NULL,
312  PLAYER_SEEN TEXT NOT NULL
313  )
314  ''')
315 except:
316  console_send(f"{e}")
317  player_send(f"{datafile}")
318  player_send(f"[color=red]CREATE TABLE player error[/color]")
319  player_send(f"exiting...")
320  sys.exit(0)
321 
322 try:
323  cursor.execute('''
324  SELECT COUNT(*) from player
325  ''')
326 except:
327  console_send(f"{e}")
328  player_send(f"{datafile}")
329  player_send(f"[color=red]SELECT FROM player error[/color]")
330  player_send(f"exiting...")
331 
332 query = cursor.fetchone()
333 players = query[0]
334 try:
335  cursor.execute('''
336  SELECT PLAYER_ID, PLAYER_TITLE FROM player
337  WHERE PLAYER_NAME = ? AND SERVER_ID = ?
338  ''', ( player_name, server_id ))
339 except:
340  console_send(f"{e}")
341  player_send(f"{datafile}")
342  player_send(f"[color=red]SELECT FROM player error[/color]")
343  player_send(f"exiting...")
344 
345 query = cursor.fetchone()
346 if query == None:
347  client_send("request player")
348 
349  for buffer in sys.stdin:
350  buffer = buffer.rstrip(os.linesep)
351  debug_send(f"{buffer}\n")
352  if regc_rqst_player_strt.match(buffer):
353  buffer = regc_rqst_player.sub('', buffer)
354  matches = regc_rqst_player_data.match(buffer)
355  player_nmbr = matches.group(1)
356  # player_name = matches.group(2)
357  player_title = matches.group(3)
358  players = players + 1
359  try:
360  cursor.execute('''
361  INSERT INTO player
362  ( PLAYER_ID, SERVER_ID, PLAYER_NAME, PLAYER_TITLE, PLAYER_SEEN )
363  VALUES ( ?, ?, ?, ?, ? )
364  ''', (players, server_id, player_name, player_title, player_seen))
365  except:
366  player_send(f"{datafile}")
367  player_send(f"[color=red]INSERT INTO player error[/color]")
368  player_send(f"exiting...")
369  sys.exit(0)
370 
371  dbConn.commit()
372  player_id = players
373  else:
374  continue
375 else:
376  player_id = query[0]
377  player_title = query[1]
378 
379 debug_send(f"player_id: {player_id}")
380 
381 try:
382  cursor.execute('''
383  UPDATE player
384  SET PLAYER_SEEN = ?
385  WHERE PLAYER_ID = ?
386  ''', (player_seen, player_id))
387 except:
388  player_send(f"{datafile}")
389  player_send(f"[color=red]UPDATE player error[/color]")
390  player_send(f"exiting...")
391  sys.exit(0)
392 
393 dbConn.commit()
394 
395 player_send(f"Hello, {player_name}")
396 
397 # map initialization #########################################################
398 #
399 # Scorn Alchemy Shop (/scorn/shops/potionshop) in The Kingdom of Scorn
400 # Created: 1996-05-02 bt (thomas@astro.psu.edu)
401 # Modified: 2023-11-27 Rick Tanner
402 #
403 # watch drawextinfo 0 10 0 Undead Church (/scorn/misc/church) in The Kingdom of Scorn
404 # watch drawextinfo 0 10 0 Created: 1993-10-15
405 # Modified: 2021-09-21 Nicolas Weeger
406 #
407 # (null) (/random/undead_quest0000) in The Kingdom of Scorn
408 # xsize -1
409 # ysize -1
410 # wallstyle dungeon2
411 # floorstyle lightdirt
412 # monsterstyle undead
413 # layoutstyle onion
414 # decorstyle creepy
415 # exitstyle sstair
416 # final_map /scorn/peterm/undead_quest
417 # symmetry 4
418 # difficulty_increase 0.000000
419 # dungeon_level 1
420 # dungeon_depth 5
421 # orientation 1
422 # origin_x 4
423 # origin_y 14
424 # random_seed 1746522242
425 #
426 regx_wtch_draw = "^watch drawextinfo (\d+\s){3}"
427 regc_wtch_draw = re.compile(regx_wtch_draw, 0)
428 regx_wtch_draw_strt = regx_wtch_draw + "([^\(]+|\(null\)\s*)\("
429 regc_wtch_draw_strt = re.compile(regx_wtch_draw_strt, 0)
430 regx_wtch_draw_name = regx_wtch_draw_strt + "[~/]"
431 regc_wtch_draw_name = re.compile(regx_wtch_draw_name, 0)
432 regx_wtch_draw_path = "^([^\(\)]+|\(null\)\s)\(([^\)]+)\)\s*(.*)"
433 regc_wtch_draw_path = re.compile(regx_wtch_draw_path, 0)
434 regx_wtch_draw_made = "Created:\s+(.*)"
435 regc_wtch_draw_made = re.compile(regx_wtch_draw_made, 0)
436 regx_wtch_draw_date = "Modified:\s+(.+)"
437 regc_wtch_draw_date = re.compile(regx_wtch_draw_date, 0)
438 regx_wtch_draw_xsiz = regx_wtch_draw + "xsize\s[-]?\d+"
439 regc_wtch_draw_xsiz = re.compile(regx_wtch_draw_xsiz, 0)
440 regx_wtch_draw_rnds = "random_seed\s\d+"
441 regc_wtch_draw_rnds = re.compile(regx_wtch_draw_rnds, 0)
442 
443 cursor.execute('''
444  CREATE TABLE IF NOT EXISTS map (
445  MAP_ID INTEGER PRIMARY KEY NOT NULL,
446  MAP_PATH TEXT UNIQUE NOT NULL,
447  MAP_NAME TEXT NOT NULL,
448  MAP_MADE TEXT NOT NULL,
449  MAP_DATE TEXT NOT NULL
450  )
451 ''')
452 
453 cursor.execute('''
454  SELECT COUNT(*) from map
455 ''')
456 query = cursor.fetchone()
457 maps = query[0]
458 
459 map_id = 0;
460 map_line = -1;
461 map_data = '';
462 map_name = '';
463 map_path = '';
464 map_made = '';
465 map_date = '';
466 
467 # quiet list initialization ##################################################
468 #
469 cursor.execute('''
470  CREATE TABLE IF NOT EXISTS quiet (
471  PLAYER_ID INTEGER,
472  SERVER_ID INTEGER,
473  MAP_PATTERN TEXT NOT NULL
474  )
475 ''')
476 
477 quiets = 0
478 
479 for loop in ( 'world_%_%', '%Apartment%', '%Inn %' ):
480  quiets = quiets + 1
481  cursor.execute('''
482  SELECT
483  MAP_PATTERN
484  FROM
485  quiet
486  WHERE
487  PLAYER_ID IS NULL AND SERVER_ID IS NULL AND MAP_PATTERN = ?
488  ''', (loop, ))
489  if cursor.fetchone() is None:
490  cursor.execute('''
491  INSERT INTO quiet
492  ( PLAYER_ID, SERVER_ID, MAP_PATTERN )
493  VALUES
494  ( NULL, NULL, ? )
495  ''', ( loop, ))
496 
497 cursor.execute('''
498  SELECT COUNT(*) from quiet
499 ''')
500 query = cursor.fetchone()
501 quiets = query[0]
502 
503 # visit log initialization ###################################################
504 #
505 cursor.execute('''
506  CREATE TABLE IF NOT EXISTS visit (
507  MAP_ID INTEGER NOT NULL,
508  PLAYER_ID INTEGER NOT NULL,
509  SERVER_ID INTEGER NOT NULL,
510  VISIT_TOTAL INTEGER NOT NULL,
511  VISIT_DATE TEXT NOT NULL,
512  COMPLETED INTEGER DEFAULT 0,
513  VISIT_DATE TEXT NOT NULL DEFAULT ""
514  )
515 ''')
516 
517 # Schema update if COMPLETED is missing.
518 #
519 cursor.execute('''
520  PRAGMA table_info(visit)
521 ''')
522 success = False
523 query = cursor.fetchall()
524 for row in query:
525  # row field numbers are zero-based
526  if row[1] == "COMPLETED":
527  success = True
528 if not success:
529  debug_send(f"ALTER TABLE visit ADD COLUMN COMPLETED INTEGER DEFAULT 0")
530  cursor.execute('''
531  ALTER TABLE visit ADD COLUMN COMPLETED INTEGER DEFAULT 0
532  ''')
533 
534 # Schema update if COMPLETED_DATE is missing.
535 #
536 cursor.execute('''
537  PRAGMA table_info(visit)
538 ''')
539 success = False
540 query = cursor.fetchall()
541 for row in query:
542  # row field numbers are zero-based
543  if row[1] == "COMPLETED_DATE":
544  success = True
545 if not success:
546  debug_send(f"ALTER TABLE visit ADD COLUMN COMPLETED_DATE STRING NOT NULL" \
547  + ": DEFAULT ''")
548  cursor.execute('''
549  ALTER TABLE visit ADD COLUMN COMPLETED_DATE STRING NOT NULL DEFAULT ""
550  ''')
551 
552 # Schema update if SERVER_ID is missing. OH NO! MAY IT NEVER BE! If the
553 # player ever played multiple servers, the old schema only tracked visits
554 # by MAP_ID and PLAYER_ID and omitted SERVER_ID. There's really not any
555 # good way to fix the visit log. The best thing to do is set SERVER_ID
556 # to the current server. Unfortunately, if the assumption is wrong, the
557 # player's only option is to clear incorrect completion data, and then
558 # effectively lose visit data for the completion on a different server.
559 #
560 # The player hopefully performs this schema update on the server with the
561 # most log entries (since that minimizes server_id errors created here if
562 # they logged visits on more than one server). Ouch!
563 #
564 cursor.execute('''
565  PRAGMA table_info(visit)
566 ''')
567 success = False
568 query = cursor.fetchall()
569 for row in query:
570  # row field numbers are zero-based
571  if row[1] == "SERVER_ID":
572  success = True
573 if not success:
574  player_send(f"[color=red]NOTE: visit database schema repair![color]")
575  player_send(f"Visit data previously failed to track the server that a " + \
576  f"visit occurred on. Unfortunately, if you logged visits " + \
577  f"on multiple servers, it is not possible to automatically " + \
578  f"determine which server was in use at the time. We are " + \
579  f"assuming (sorry) that the current server is the one that " + \
580  f"should be used for all old visit data. While playing, if" + \
581  f" you notice incorrect completion data upon entering a map" + \
582  f", use 'scripttell {codepath} incomplete' to erase it. If" + \
583  f" the map was completed on another server, logon to it and" + \
584  f" re-visit the map and mark it complete (again) there." \
585  )
586  debug_send(f"ALTER TABLE visit ADD COLUMN SERVER_ID INTEGER DEFAULT " + \
587  f"{server_id}")
588  cursor.execute('''
589  ALTER TABLE visit ADD COLUMN SERVER_ID INTEGER DEFAULT ''' + \
590  f"{server_id}")
591 
592 visits = 0
593 visit_date = ''
594 
595 cursor.execute('''
596  SELECT COUNT(*) from visit
597 ''')
598 query = cursor.fetchone()
599 visits = query[0]
600 
601 # visit cache initialization #################################################
602 #
603 vcConn = sqlite3.connect(":memory:")
604 vcursor = vcConn.cursor()
605 
606 vcursor.execute('''
607  CREATE TABLE IF NOT EXISTS vcache (
608  MAP_SEQ INTEGER NOT NULL,
609  MAP_ID INTEGER NOT NULL
610  )
611 ''')
612 
613 v_head = 1
614 v_tail = 0
615 
616 # cfmaplog service ###########################################################
617 
618 regc_scripttell = re.compile('^scripttell\s+', 0)
619 
620 # Notify when entering a map.
621 #
622 debug_send("watch newmap")
623 client_send("watch newmap")
624 
625 # Interact with the client.
626 #
627 for buffer in sys.stdin:
628  buffer = buffer.rstrip(os.linesep)
629 
630  debug_send(f"> '{buffer}'")
631 
632  if not len(buffer):
633  time.sleep(0.25)
634  continue
635 
636  if regc_scripttell.match(buffer):
637  match regc_scripttell.sub('', buffer).split(os.linesep):
638 
639  # Player may halt the script with either 'scripttell' or 'scriptkill'
640  #
641  case ['quit']:
642  break
643 
644  # player may toggle debug with 'scripttell'
645  #
646  case ['debug']:
647  debug = not debug
648 
649  case ['completed']:
650  try:
651  cursor.execute('''
652  SELECT v.COMPLETED_DATE, v.COMPLETED, m.MAP_NAME, m.MAP_PATH
653  FROM visit v
654  JOIN server s ON s.SERVER_ID = v.SERVER_ID
655  JOIN player p ON p.PLAYER_ID = v.PLAYER_ID
656  JOIN map m ON m.MAP_ID = v.MAP_ID
657  WHERE v.PLAYER_ID = ? AND
658  v.SERVER_ID = ? AND
659  v.COMPLETED >= 1
660  ORDER BY COMPLETED_DATE DESC
661  ''', ( player_id, server_id ))
662  except:
663  console_send(f"{e}")
664 
665  player_send(f"You marked the following maps as completed:")
666  player_send_bare(f"Last Completion : ### : Map Name : /Map/Path")
667  query = cursor.fetchall()
668  for row in query:
669  player_send_bare(f"{row[0]} : {row[1]:03d} : {row[2]} : {row[3]}")
670 
671  case ['complete' | 'incomplete']:
672  completed = 0
673  completed_date = ""
674  try:
675  cursor.execute('''
676  SELECT COMPLETED, COMPLETED_DATE
677  FROM visit WHERE MAP_ID = ? and PLAYER_ID = ?
678  ''', ( map_id, player_id))
679  except:
680  console_send(f"{e}")
681  debug_send(f"map_id {map_id} player_id {player_id}")
682 
683  query = cursor.fetchone()
684  if query == None:
685  if buffer == 'scripttell complete':
686  completed = 1
687  completed_date = time.strftime("%Y/%m/%d %H:%M")
688  else:
689  if buffer == 'scripttell complete':
690  completed = query[0] + 1
691  player_send(f"You've marked this map complete {completed} times.")
692  if completed > 1 and len(query[1]):
693  player_send(f"The most recent previous time was: {query[1]}.")
694  completed_date = time.strftime("%Y/%m/%d %H:%M")
695  else:
696  if query[0] == 0:
697  player_send(f"This map is already marked as not completed.")
698  else:
699  player_send(f"This map is now marked as not completed.")
700 
701  debug_send(f"map_id {map_id} player_id {player_id}")
702  debug_send(f"completed {completed} completed_date {completed_date}")
703  try:
704  cursor.execute('''
705  UPDATE visit
706  SET COMPLETED = ?, COMPLETED_DATE = ?
707  WHERE MAP_ID = ? AND PLAYER_ID = ?
708  ''', (completed, completed_date, map_id, player_id))
709  except:
710  console_send(f"{e}")
711 
712  dbConn.commit()
713 
714  case ['help', *arguments]:
715  helptext = """
716 A Crossfire RPG client plug-in that keeps and reports statistics related to map
717 visits. Statistics are kept separate for different characters, and are unique
718 per-server played. Additionally, it serves as a utility to track which maps
719 have been 'completed'. Statistics include the date and time of the most recent
720 visit and completion, if any. A number of reporting options are supported to
721 allow the player to review the collected data.
722 
723 The player interacts with the plug-in via 'scripttell' commands. Supported
724 commands are:
725 
726 * completed
727  While in a map, mark it to show that you believe you have 'finished' all you
728  want to do. What you consider 'finished' is entirely up to you. A tally is
729  kept of the number of times the command is used on each map. It is usually
730  most helpful to mark the entrance map 'completed' since the number of times
731  the map was 'completed' is shown upon entry after the tally is greater than
732  zero. Unfortunately, when the entrance map is a random map, this does not
733  presently work since the entry map path may change from visit to visit.
734  That said marking it anyway assures the completion is reported on entry to
735  the map in the event the same map is shown first in the future. For these
736  maps, one could mark every level, but probably most importantly, the last
737  non-random map of the dungeon.
738 
739 * help
740  This information.
741 
742 * debug
743  Enable console (not in-game) messages. To see these messages, start the
744  client in a console. This is a toggle, so entering the command another
745  time disables the messages.
746 
747 * incomplete
748  Clear the tally of a particular 'completed' map because one was erroneously
749  marked as complete, or, due to discovering that something was missed on the
750  objectives of the map. Using this command means no 'completed' tally is
751  shown on entry.
752 
753 * quit
754  Stop the plugin. Basically, what 'scriptkill' does, except that the script
755  initiates. At present, the plugin doesn't know how to 'catch' a scriptkill
756  (if that is even possible).
757 
758 * visited
759 * visited <MaxToShow>
760  List all, or up to <MaxToShow> entries, in date order, with recent first.
761  <MaxToShow> is an optional integer.
762 
763 * visited least
764 * visited least <MaxToShow>
765  List all, or up to <MaxToShow> entries, with lowest number of visits as the
766  primary sort key, and date order for ties, with the most recent first.
767 
768 * visited most
769 * visited most <MaxToShow>
770  List all, or up to <MaxToShow> entries, with highest number of visits as the
771  primary sort key, and date order for ties, with the most recent first.
772 """
773  player_send_bare(helptext)
774 
775  # The bare 'visted' command MUST come last in the match cases.
776  #
777  case ['visited', *arguments]:
778  sqlcmd = '''
779  SELECT v.VISIT_DATE, v.VISIT_TOTAL, m.MAP_NAME, m.MAP_PATH
780  FROM visit v
781  JOIN server s ON s.SERVER_ID = v.SERVER_ID
782  JOIN player p ON p.PLAYER_ID = v.PLAYER_ID
783  JOIN map m ON m.MAP_ID = v.MAP_ID
784  WHERE v.PLAYER_ID = ? AND
785  v.SERVER_ID = ? AND
786  v.VISIT_TOTAL >= 1'''
787 
788  match buffer.split():
789  case ['scripttell', 'visited', 'least', *limit]:
790  sqlcmd = sqlcmd + '''
791  ORDER BY v.VISIT_TOTAL ASC, VISIT_DATE ASC'''
792  case ['scripttell', 'visited', 'most', *limit]:
793  sqlcmd = sqlcmd + '''
794  ORDER BY v.VISIT_TOTAL DESC, VISIT_DATE DESC'''
795  case ['scripttell', 'visited', *limit]:
796  sqlcmd = sqlcmd + '''
797  ORDER BY v.VISIT_DATE DESC'''
798 
799  match len(limit):
800  case 0:
801  pass
802  case 1:
803  if not limit[0].isnumeric():
804  player_send(f"[color=red]LIMIT argument must be numeric." + \
805  f"[\color]")
806  continue
807  else:
808  sqlcmd = sqlcmd + '''
809  LIMIT ''' + f"{limit[0]}"
810  case _:
811  player_send(f"[color=red]Only one LIMIT argument wanted." + \
812  f"[\color]")
813  continue
814 
815  debug_send(f'''{sqlcmd}, ({player_id}, {server_id})''')
816 
817  try:
818  cursor.execute(sqlcmd, (player_id, server_id))
819  except sqlite3.Error as e:
820  console_send(f"{e}")
821 
822  player_send(f"All recorded visits:")
823  player_send_bare(f"Most Recent Visit : ### : Map Name : /Map/Path")
824  query = cursor.fetchall()
825  for row in query:
826  player_send_bare(f"{row[0]} : {row[1]:04d} : {row[2]} : {row[3]}")
827 
828  case _:
829  player_send(f"'{buffer}'")
830  player_send(f"[color=red]Not a recognized command.[\color]")
831 
832  buffer = ''
833  continue
834 
835  # When a map is entered, ask the client to start forwarding drawextinfo
836  # the server sends. Instruct the client to issue a mapinfo command on
837  # our behalf. Then commence listening for mapinfo output the client
838  # forwards to us.
839  #
840  if buffer == "watch newmap":
841  buffer = ''
842  map_id = 0;
843  map_line = 0;
844  map_data = '';
845  map_name = '';
846  map_path = '';
847  map_made = '';
848  map_date = '';
849  client_send("watch drawextinfo")
850  client_send("issue 1 1 mapinfo")
851  continue
852 
853  if map_line >= 0:
854  map_line = map_line + 1
855 
856  debug_send(f"{map_line}> '{buffer}'")
857 
858  if regc_wtch_draw_name.match(buffer):
859  buffer = regc_wtch_draw.sub('', buffer)
860  map_data = "map_name"
861  elif regc_wtch_draw_made.search(buffer):
862  buffer = regc_wtch_draw.sub('', buffer)
863  map_data = "map_made"
864  elif regc_wtch_draw_date.search(buffer):
865  buffer = regc_wtch_draw.sub('', buffer)
866  map_data = "map_date"
867  elif regc_wtch_draw_xsiz.search(buffer):
868  buffer = regc_wtch_draw.sub('', buffer)
869  map_date = time.strftime("%Y-%m-%d")
870  map_data = "map_xsize"
871  map_made = 'random'
872  elif regc_wtch_draw_rnds.match(buffer):
873  map_data = "map_write"
874  else:
875  buffer = ''
876  continue
877 
878  debug_send(f"map_data {map_data}")
879 
880  match map_data:
881  case 'map_name':
882  matches = regc_wtch_draw_path.match(buffer)
883  debug_send(f"{map_line}_1: '" + matches.group(1) + "'")
884  debug_send(f"{map_line}_2: '" + matches.group(2) + "'")
885  debug_send(f"{map_line}_3: '" + matches.group(3) + "'")
886 
887  map_name = matches.group(1) + matches.group(3)
888  map_path = matches.group(2)
889  continue
890  case 'map_made':
891  matches = regc_wtch_draw_made.search(buffer)
892  map_made = matches.group(1)
893  buffer = ''
894  continue
895  case 'map_date':
896  matches = regc_wtch_draw_date.search(buffer)
897  map_date = matches.group(1)
898  map_data = 'map_write'
899 
900  if map_data == 'map_write':
901 
902  debug_send(f"map_name (map_path) {map_name} ({map_path})")
903  debug_send(f"map_made {map_made}")
904  debug_send(f"map_date {map_date}")
905  debug_send(f"unwatch: drawextinfo")
906 
907  client_send("unwatch drawextinfo")
908 
909  # Insert a "new" map into the map database. Assume if the insertion
910  # fails that it was already present. TODO: Do not insert if unneeded.
911  #
912  try:
913  maps = maps + 1
914  cursor.execute('''
915  INSERT INTO map
916  ( MAP_ID, MAP_PATH, MAP_NAME, MAP_MADE, MAP_DATE )
917  VALUES ( ?, ?, ?, ?, ? )
918  ''', (maps, map_path, map_name, map_made, map_date))
919  dbConn.commit()
920  except sqlite3.IntegrityError:
921  maps = maps - 1
922 
923  # Get the map_id of the entered map. This SHOULD NEVER fail because the
924  # map should always have been added by this point.
925  #
926  cursor.execute('''
927  SELECT MAP_ID FROM map
928  WHERE MAP_PATH = ?
929  ''', ( map_path, ))
930  query = cursor.fetchone()
931  if query == None:
932  map_id = 0
933  pass
934  else:
935  map_id = query[0]
936 
937  debug_send(f"map_id: {map_id}")
938 
939  # Is the map_id in the (recently) visit(ed) cache? If so, do not make
940  # an attempt to log the visit. This should reduce spammy visits to
941  # maps that may occur when passing through doors inside a store, or
942  # perhaps even for events like dimention door.
943  #
944  vcursor.execute ('''
945  SELECT MAP_SEQ FROM vcache
946  WHERE MAP_ID = ?
947  ''', ( map_id, ) )
948  query = vcursor.fetchone()
949  if query != None:
950  debug_send(f"SQUELCH visit message!")
951  #
952  # But completion notices are never squelched if for recent visits as
953  # this may affect a player's choice as to whether or not to proceed.
954  #
956  else:
957  vcursor.execute('''
958  INSERT INTO vcache
959  ( MAP_SEQ, MAP_ID )
960  VALUES ( ?, ? )
961  ''', (v_head, map_id) )
962  dbConn.commit()
963  v_head = v_head + 1
964  debug_send(f"vcache -> v_head: {v_head}")
965  if v_head - v_tail >= 10:
966  vcursor.execute('''
967  DELETE FROM vcache
968  WHERE MAP_SEQ = ?
969  ''', (v_tail, ))
970  dbConn.commit()
971  v_tail = v_tail + 1
972  debug_send(f"vcache -> v_tail: {v_tail}")
973 
974  # Is there already a visit log for this map and player? This may fail
975  # because a player hasn't visited the map before when the logger was
976  # active. If not found, add a new visit log, otherwise update the
977  # existing log.
978  #
979  visit_date = time.strftime("%Y/%m/%d %H:%M")
980  cursor.execute('''
981  SELECT VISIT_TOTAL, COMPLETED, COMPLETED_DATE FROM visit
982  WHERE MAP_ID = ? AND PLAYER_ID = ?
983  ''', ( map_id, player_id ))
984  query = cursor.fetchone()
985  if query == None:
986  visit_total = 0
987  cursor.execute('''
988  INSERT INTO visit
989  ( MAP_ID, PLAYER_ID, VISIT_TOTAL, VISIT_DATE )
990  VALUES ( ?, ?, ?, ? )
991  ''', (map_id, player_id, 1, visit_date))
992  dbConn.commit()
993  player_send(f"I don't think I remember this place!")
994  else:
995  completed_date = query[2]
996  visit_total = query[0]
997  completed = query[1]
998  cursor.execute('''
999  SELECT
1000  MAP_PATTERN
1001  FROM
1002  quiet
1003  WHERE PLAYER_ID IS NULL AND SERVER_ID IS NULL AND ? LIKE MAP_PATTERN
1004  ''', ( map_name, ))
1005  query = cursor.fetchone()
1006  if query:
1007  debug_send(f"QUIET!SHH!")
1008  else:
1009  player_send(f"You were here at least {visit_total} times prior.")
1010  #
1011  # Update the visit count
1012  #
1013  visit_total = visit_total + 1
1014  debug_send(f"visit_total {visit_total}")
1015 
1016  try:
1017  cursor.execute('''
1018  UPDATE visit
1019  SET VISIT_TOTAL = ?, VISIT_DATE = ?
1020  WHERE MAP_ID = ? AND PLAYER_ID = ?
1021  ''', (visit_total, visit_date, map_id, player_id))
1022  except:
1023  console_send(f"{e}")
1024  dbConn.commit()
1025  #
1026  # Always check map completion status without regard to quiet status.
1027  #
1028  check_completed()
1029 
1030  map_line = -1;
1031 
1032  buffer = ''
1033 
1034 player_send(f"Farewell, {player_name}")
1035 
1036 vcConn.close()
1037 dbConn.close()
1038 
1039 # End ########################################################################
1040 
1041 sys.exit(0)
1042 
by
cfmaplog py A Crossfire Map Log Plug In designed for the Crossfire GTK2 Client Conceptualized by
Definition: cfmaplog.txt:4
cfmaplog.player_send_bare
def player_send_bare(text)
Definition: cfmaplog.py:149
installer
To create a Windows client installer
Definition: nsis.txt:1
cfmaplog.check_completed
def check_completed()
Definition: cfmaplog.py:157
in
static GInputStream * in
Definition: client.c:71
client
To create a Windows client you need NSIS First build the client
Definition: nsis.txt:3
cfmaplog.debug_send
def debug_send(text)
Definition: cfmaplog.py:63
cfmaplog.client_send
def client_send(text)
Definition: cfmaplog.py:47
cfmaplog.player_send
def player_send(text)
Definition: cfmaplog.py:141
needed
To create a Windows client you need NSIS First build the then put all its required files into a files subdirectory of the directory containing this readme if needed
Definition: nsis.txt:5
cfmaplog.console_send
def console_send(text)
Definition: cfmaplog.py:55
script
Definition: script.c:120