1#!/usr/bin/env python
2# -*- Mode: Python; tab-width: 4 -*-
3#
4# Copyright (C) 2001 Gianluigi Tiesi <sherpya@netfarm.it>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
13# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14# for more details.
15#
16# Ranking patch by <JEBs@shbe.net> 20020503
17# Small "unsigned int" fix by <JEBs@shbe.net> 20020818
18#
19# ==========================================================================
20__version__ = "0.9"
21
22
23### Only if CGI_MODE = 1
24CGI_MODE=0
25FILE="/opt/bnetd/var/ladders/ladder.D2DV"
26MAX=100
27
28from struct import unpack,calcsize
29from string import find,split,join
30from os import stat
31from sys import argv,exit,stdout
32from getopt import getopt
33
34
35#### Templates
36modes = [ 'html', 'ansi', 'ascii', 'python' ]
37templates = {}
38for m in modes:
39    templates[m] = {}
40
41
42### html ###
43
44#
45templates['html']['header']="""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
46<html>
47 <head>
48  <title>D2 Closed Realm Ladder</title>
49  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
50 </head>
51 <body bgcolor="#000000" text="#ffff00">
52 <h2 style="color: lightgreen;" align="center">D2 Closed Realm Ladder</h2>
53  <table style="border: solid lightblue; border-width: 1px;" align="center" border="0" width="80%" summary="">
54"""
55
56#
57templates['html']['footer']="""  </table>
58  <p style="color: lightblue;" align="center">Generated by ladder.py v %s - &copy; 2001 <a style="color: lightgreen;" href="mailto:sherpya@netfarm.it">Sherpya</a></p>
59 </body>
60</html>
61""" % __version__
62
63# %s for description of ladder type
64templates['html']['summary'] = """    <tr style="color: lightblue" bgcolor="#666666"><th colspan="5">Ladder for %s</th></tr>
65"""
66
67#
68templates['html']['tbheader'] = """<tr style="color: lightgreen;"><th align="center">#</th><th align="left">Charname</th><th align="right">level</th><th align="center">class</th><th align="right">exp</th></tr>
69"""
70# %s for charname
71templates['html']['normal'] = """%s"""
72templates['html']['hardcore'] = { 0 : """<span style="color: red;">%s</span>""",
73                                  1 : """<span style="color: orange;">%s</span>""" }
74
75# %s charname - %d level - %s class - %d experience
76templates['html']['entry'] = """<tr bgcolor="#222222"><td align="right">%d</td><td align="left">%s</td><td align="right">%d</td><td align="center">%s</td><td align="right">%d</td></tr>
77"""
78
79#
80templates['html']['separator'] = """<tr><td colspan="5">&nbsp;</td></tr>
81"""
82
83#### html
84
85#### ascii / ansi
86line = '-' * 59 + '\n'
87s10 = ' ' * 10
88s14 = ' ' * 14
89s5 = ' ' * 5
90text = 'D2 Closed Ladder'
91esc = '\033'
92off = esc + '[0m'
93
94colors = {
95        'grey': esc + '[1;30m',
96        'red': esc + '[1;31m',
97        'green': esc + '[1;32m',
98        'yellow': esc + '[1;33m',
99        'blue': esc + '[1;34m',
100        'purple': esc + '[1;35m',
101        'magenta': esc + '[1;36m',
102        'white': esc + '[1;37m',
103        'green': esc + '[1;32m'
104        }
105
106templates['ascii']['header'] = line + (int((len(line) - len(text))/ 2)) * ' ' + text + '\n' + line
107templates['ascii']['footer'] = 'generated by ladder.py (c) Sherpya [sherpya@netfarm.it]\n'
108templates['ascii']['summary'] = 'Ladder for %s\n\n'
109templates['ascii']['tbheader'] = '  # charname' + s14 + 'level' + s10 + 'class' + s10 + 'exp' + '\n\n'
110templates['ascii']['normal'] = '%s'
111templates['ascii']['hardcore'] = { 0 : '*%s', 1: '^%s' }
112templates['ascii']['entry'] = '%3d %-23s %2d %16s  %10d\n'
113templates['ascii']['separator'] = line + '\n'
114
115line = colors['blue'] + ( '-' * 59) + off + '\n'
116templates['ansi']['header'] = line + (int((len(line) - len(text) - 10)/ 2)) * ' ' + colors['green'] + text + off + '\n' + line
117templates['ansi']['footer'] = colors['green'] + 'generated by ' + colors['blue'] + 'ladder.py' + colors['green'] + ' (c) Sherpya [sherpya@netfarm.it]' + off + '\n'
118templates['ansi']['summary'] = colors['white'] + 'Ladder for %s' + off + '\n\n'
119templates['ansi']['tbheader'] = colors['green'] + '  # charname' + s14 + 'level' + s10 + 'class' + s10 + 'exp' + off + '\n\n'
120templates['ansi']['normal'] = colors['yellow'] + '%s'
121templates['ansi']['hardcore'] = { 0 : colors['red'] + '%s', 1: colors['grey'] + '%s' }
122templates['ansi']['entry'] = colors['yellow'] + '%3d %-30s %2d %16s  %10d' + off + '\n'
123templates['ansi']['separator'] = line + '\n'
124
125
126del text
127#### ascii / ansi
128
129
130
131### Some struct from d2cs/d2dbs source
132#
133# ladder header (4 + 4 = 8):
134#   bn_int maxtype
135#   bn_int checksum
136LD_HEAD="<2i"
137szLD_HEAD = calcsize(LD_HEAD)
138
139#
140# ladder info (4 + 2 + 1 + 1 + 16 = 24):
141#   bn_int experience
142#   bn_short status
143#   bn_byte level
144#   bn_byte class;
145#   char charname[16];
146LD_INFO="<Ihbb16s"
147szLD_INFO = calcsize(LD_INFO)
148
149#
150# ladder index (4 + 4 + 4 = 12):
151#   bn_int type
152#   bn_int offset
153#   bn_int number
154LD_INDEX="<3i"
155szLD_INDEX = calcsize(LD_INDEX)
156
157## Status flags
158S_INIT = 0x1
159S_EXP  = 0x20
160S_HC   = 0x04
161S_DEAD = 0x08
162
163
164classes = {
165    0x00 : ['Amazon', 'f'],
166    0x01 : ['Sorceress', 'f'],
167    0x02 : ['Necromancer', 'm'],
168    0x03 : ['Paladin', 'm'],
169    0x04 : ['Barbarian', 'm'],
170    0x05 : ['Druid', 'm'],
171    0x06 : ['Assassin', 'f']
172    }
173
174desc = {
175    'nor': 'Diablo II',
176    'exp': 'Lord of Desctruction'
177    }
178
179
180diff = {
181    'nor': {
182        0x1: { 0 : { 'm': 'Sir', 'f': 'Dame' },
183               1 : { 'm': 'Count', 'f': 'Countess' }
184               },
185
186        0x2: { 0 : { 'm': 'Lord', 'f': 'Lady' },
187               1 : { 'm': 'Duke', 'f': 'Duchess' }
188               },
189
190        0x3: { 0 : { 'm': 'Baron', 'f': 'Baroness' },
191               1 : { 'm': 'King', 'f': 'Queen' }
192               }
193        },
194
195    'exp': {
196        0x1: { 0 : { 'm': 'Slayer', 'f': 'Slayer' },
197               1 : { 'm': 'Destroyer', 'f': 'Destroyer' }
198               },
199
200        0x2: { 0 : { 'm': 'Champion', 'f': 'Champion' },
201               1 : { 'm': 'Conqueror', 'f': 'Conqueror' }
202               },
203
204        0x3: { 0 : { 'm': 'Patriarch', 'f': 'Matriarch' },
205               1 : { 'm': 'Guardian', 'f': 'Guardian' }
206               }
207        }
208    }
209
210## Utils
211
212
213def remove_null(text):
214    return split(text, chr(0))[0]
215
216
217def get_ladder(file):
218    try:
219        size = stat(file)[6]
220        data = open(file, "rb")
221    except:
222        print "Error opening %s for read" % file
223        exit()
224
225    maxtype, checksum = unpack(LD_HEAD, data.read(szLD_HEAD))
226
227    size = size - szLD_HEAD
228
229    head = []
230
231    for i in range(maxtype):
232        type, offset, number = unpack(LD_INDEX, data.read(szLD_INDEX))
233        size = size - szLD_INDEX
234        head.append(
235        {
236        'type': type,
237        'offset': offset,
238        'number': number
239        })
240
241
242    ladder = {}
243    ladder['nor'] = []
244    ladder['exp'] = []
245
246    temp = {}
247    temp['nor'] = []
248    temp['exp'] = []
249
250
251    while size > 0:
252        try:
253            experience, status, level, _class, charname = unpack(LD_INFO, data.read(szLD_INFO))
254        except:
255            ### Bad data
256            size = size - szLD_INFO
257            continue
258
259        size = size - szLD_INFO
260
261        ## Avoid null chars
262        if not experience:
263            continue
264
265        charname = remove_null(charname)
266        died = 0
267
268        if status & S_EXP:
269            _type = 'exp'
270            difficulty = ((status >> 0x08) & 0x0f) / 5
271        else:
272            _type = 'nor'
273            difficulty = ((status >> 0x08) & 0x0f) / 5
274
275        if status & S_HC:
276            hc = 1
277            if status & S_DEAD:
278                died = 1
279        else:
280            hc = 0
281
282        c_class = classes[_class]
283
284        if difficulty and diff[_type].has_key(difficulty):
285            prefix = diff[_type][difficulty][hc][c_class[1]]
286        else:
287            prefix = None
288
289        char = (experience, {
290            'charname'   : charname,
291            'prefix'     : prefix,
292            'experience' : experience,
293            'class'      : c_class[0],
294            'sex'        : c_class[0],
295            'level'      : level,
296            'type'       : _type,
297            'difficulty' : difficulty,
298            'hc'         : hc,
299            'died'       : died
300            })
301        ## Dupe char? why?
302        if char not in temp[_type]:
303            temp[_type].append(char)
304
305    data.close()
306
307    ## Sorting by exp
308    temp['nor'].sort()
309    temp['nor'].reverse()
310    temp['exp'].sort()
311    temp['exp'].reverse()
312
313    for _type in temp.keys():
314        for ch in temp[_type]:
315            ladder[_type].append(ch[1])
316    del temp
317
318    return ladder
319
320def generate(ladder, mode, output, max):
321
322    output.write(templates[mode]['header'])
323
324    for _type in ladder.keys():
325        count = 1
326        output.write(templates[mode]['summary'] % desc[_type])
327        output.write(templates[mode]['tbheader'])
328
329        for ch in ladder[_type]:
330            if ch['prefix']:
331                charname = "%s %s" % (ch['prefix'], ch['charname'])
332            else:
333                charname = ch['charname']
334
335            if ch['hc']:
336                charname = templates[mode]['hardcore'][ch['died']] % charname
337            else:
338                charname = templates[mode]['normal'] % charname
339
340            output.write(templates[mode]['entry'] % (count, charname, ch['level'], ch['class'], ch['experience']))
341            count = count + 1
342            if count > max:
343                break
344
345        output.write(templates[mode]['separator'])
346
347    output.write(templates[mode]['footer'])
348
349
350def pickle_to(ladder, output):
351    try:
352        from cPickle import dump
353    except:
354        from pickle import dump
355
356    try:
357        out = open(output, "wb")
358    except:
359        print "Cannot open %s for pickle dump" % output
360        exit()
361
362    dump(ladder, out)
363    out.close()
364
365
366### Main
367
368### CGI MODE
369if CGI_MODE:
370    print "Content-Type: text/html"
371    print
372    ladder = get_ladder(FILE)
373    generate(ladder, 'html', stdout, MAX)
374    exit()
375
376args = argv[1:]
377optlist, args = getopt(args, "hi:o:m:n:")
378if len(args):
379    for bad in args:
380        print "%s: Unrecognized option %s" % (argv[0], bad)
381    exit()
382
383### defaults
384file = None
385output = None # stdout
386mode = modes[0]
387real_max = 1000
388max = 100
389
390def show_help():
391    print
392    print "ladder.py v%s - (c) 2001 Sherpya <sherpya@netfarm.it>" % __version__
393    print "Usage: ladder.py -i ladder_file [-o outputfile] [-m mode] [-n max ladder chars]"
394    print
395    print "       -i ladder_file, is the ladder file like ladder.D2DV"
396    print "       -o output file, if omitted defaults to stdout"
397    print "       -m mode, avaiables mode are: %s, defaults to %s" % (join(modes,', '), modes[0])
398    print "       -n max_char, max char to display in each ladder, defaults to %d" % max
399    print
400    print "       note: python output mode creates a python object usable by pickle module"
401    print
402
403for opt in optlist:
404
405    # Help
406    if opt[0] == '-h':
407        show_help()
408        exit()
409
410    # Input file
411    if opt[0] == '-i':
412        file = opt[1]
413        continue
414
415    # Output file
416    if opt[0] == '-o':
417        output = opt[1]
418        continue
419
420    # Output mode (html, ansi, ascii, python)
421    if opt[0] == '-m':
422        if opt[1] in modes:
423            mode = opt[1]
424            continue
425        else:
426            print "Invalid mode %s, valid modes are %s" % (opt[1], join(modes, ', '))
427            exit()
428
429    # Max chars in ladder
430    if opt[0] == '-n':
431        try:
432            max = int(opt[1])
433        except:
434            max = 0
435
436        if (max < 2) or max > real_max:
437            print "Invalid value for max char in ladder must be > 1 and < %d" % real_max
438            exit()
439        continue
440
441if not file:
442    show_help()
443    exit()
444
445ladder = get_ladder(file)
446if mode == 'python':
447    if output:
448        pickle_to(ladder, output)
449    else:
450        print "Cannot dump python object to stdout"
451    exit()
452
453if output:
454    try:
455        output = open(output, "wb")
456    except:
457        print "Cannot open %s for writing" % output
458        exit()
459else:
460    output = stdout
461
462generate(ladder, mode, output, max)
463
464
465