Browse Source

Initial commit

nosoop 2 years ago
commit
9d16345337
5 changed files with 442 additions and 0 deletions
  1. 3 0
      .gitignore
  2. 25 0
      LICENSE
  3. 36 0
      README.md
  4. 173 0
      tf_attr_list.py
  5. 205 0
      tf_item_list.py

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+# ignore config file
+config.ini
+

+ 25 - 0
LICENSE

@@ -0,0 +1,25 @@
+BSD 2-Clause License
+
+Copyright (c) 2021, nosoop
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 36 - 0
README.md

@@ -0,0 +1,36 @@
+# TF2 Schema Page Renderer
+
+Set of scripts to generate no-frills HTML pages for Team Fortress 2 item information.
+
+In memory of optf2.
+
+I host semi up-to-date renders for [items](https://csrd.science/misc/econ-tf/items.html) and
+[attributes](https://csrd.science/misc/econ-tf/attributes.html).
+
+## Configuration
+
+The scripts require the [chevron][] library.
+
+Create a `config.ini` value with the below contents, populating the values as appropriate
+(values are unquoted):
+
+```
+[DEFAULT]
+key = YOUR_STEAM_API_KEY_HERE
+language = en
+```
+
+Run `python3 tf_attr_list.py` to generate `attributes.html`; `python3 tf_item_list.py` to
+generate `items.html`.  The scripts will fetch the latest version of the schema on each run.
+
+[chevron]: https://github.com/noahmorrison/chevron
+
+## License and Credits
+
+The scripts are released under the 2-Clause BSD License.
+
+[chevron][] is an implementation of the mustache templating language.  It is released under the
+MIT License.
+
+[tablesort](https://github.com/tristen/tablesort) is used to provide optional sorting
+functionality to the tables.  It is released under the MIT License.

+ 173 - 0
tf_attr_list.py

@@ -0,0 +1,173 @@
+#!/usr/bin/python3
+
+import base64
+import chevron
+import configparser
+import hashlib
+import html.parser
+import io
+import json
+import urllib
+import urllib.request
+import urllib.parse
+
+template_string = r'''
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<title>TF2 Attribute List</title>
+	
+	<style>
+	html { scroll-behavior: smooth; }
+	body {
+		font-family: Verdana,Arial,sans-serif;
+		font-size: 0.75em;
+		
+		background-color: #111;
+		color: #FFF;
+	}
+	td { padding: 0.25em; }
+	a { color: #1eb; }
+	.attribute {
+		vertical-align: top;
+	}
+	.header {
+		font-weight: bold;
+		position: sticky;
+		top: 0;
+		background-color: #111;
+	}
+	.value_type {
+		overflow: hidden;
+	}
+	.footer {
+		margin-top: 2.5em;
+		text-align: center;
+	}
+	.positive { color: #28F; }
+	.negative { color: #F00; }
+	
+	tr:nth-child(even) { background: #222; }
+	tr:hover { background: #333; }
+	
+	tr:target {
+		box-shadow: 0 0 1em .25em #abb;
+		position: relative;
+	}
+	.id a {
+		color: inherit;
+		text-decoration: none;
+		display: block;
+	}
+	</style>
+</head>
+<body>
+	<h1>List of Team Fortress 2 attributes:</h1>
+	
+	<p>Page generated for schema dated {{update_time}}.  <a href="" download>Click here to download this page for offline viewing.</a></p>
+	
+	<table class="attribute-list">
+		<thead>
+			<tr class="attribute header">
+				<th class="id">ID</th>
+				<th class="name">Name</th>
+				<th class="description">Description</th>
+				<th class="is_hidden">Hidden</th>
+				<th class="value_type">Value Type</th>
+				<th class="class_name">Class</th>
+			</tr>
+		</thead>
+		<tbody>
+			{{#result.attributes}}
+			<tr class="attribute entry" id="{{defindex}}">
+				<td class="id"><a href="#{{defindex}}">{{defindex}}</a></td>
+				<td class="name {{effect_type}}">{{name}}</td>
+				<td class="description">{{description_string}}</td>
+				<td class="is_hidden">{{#hidden}}true{{/hidden}}{{^hidden}}false{{/hidden}}</td>
+				<td class="value_type">{{description_format}}</td>
+				<td class="class_name">{{attribute_class}}</td>
+			</tr>
+			{{/result.attributes}}
+		</tbody>
+	</table>
+	<div class="footer">
+		<div>Generated with <a href="https://git.csrd.science/nosoop/py-tf2-schema-renderer">this script</a>.</div>
+		<div>Table sorting provided by <a href="https://github.com/tristen/tablesort">tablesort</a>.</div>
+	</div>
+	
+	<!--
+	  - table sorting functionality
+	  - the page degrades gracefully without JS, but sorting is pretty nice to have
+	  -->
+	{{#inline_scripts}}
+	<script src="https://unpkg.com/tablesort@5.0.2/dist/tablesort.min.js"
+			integrity="sha384-MAoWp5qWRu0089eW0evm9gRng72sxH0daE2mkSXN8RoQ99hG/x+Wi8GxUbrgbYLp"
+			crossorigin="anonymous"></script>
+	<script src="https://unpkg.com/tablesort@5.0.2/dist/sorts/tablesort.number.min.js"
+			integrity="sha384-YPqBWGhq2JCwnOo6VsMYgE5XvUqQrzskaIGxc7CByfV6o9viRVq8uAhCs/0iPQBw"
+			crossorigin="anonymous"></script>
+	{{/inline_scripts}}
+	<script>
+		new Tablesort(document.querySelector('.attribute-list'));
+		window.onload = window.onhashchange = () => {
+			if (location.hash) {
+				document.getElementById(location.hash.slice(1)).scrollIntoView({
+					behavior: "auto", block: "center", inline: "center"
+				});
+			}
+		};
+	</script>
+</body>
+</html>
+'''
+
+def get_schema_overview(params):
+	opts = dict(params.items())
+	url = "https://api.steampowered.com/IEconItems_440/GetSchemaOverview/v0001/"
+	with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(opts)}") as r:
+		return { **json.load(r), 'update_time': r.info().get('last-modified') }
+
+def inline_scripts(text, render):
+	"""
+	Helper function to parse script elements, download the scripts and inline them.
+	"""
+	class ScriptElementParser(html.parser.HTMLParser):
+		def __init__(self):
+			super(ScriptElementParser, self).__init__()
+			self.scripts = []
+			self.current = []
+		
+		def handle_starttag(self, tag, attrs):
+			if tag == 'script':
+				self.current.append(dict(attrs))
+		
+		def handle_endtag(self, tag):
+			self.scripts.append(self.current.pop())
+	
+	parser = ScriptElementParser()
+	parser.feed(text)
+	
+	f = io.StringIO()
+	for script in parser.scripts:
+		with urllib.request.urlopen(script['src']) as r:
+			data = r.read()
+			if 'integrity' in script:
+				ct, hash = script['integrity'].split('-', 1)
+				h = hashlib.new(ct)
+				h.update(data)
+				if base64.b64encode(h.digest()).decode('ascii') != hash:
+					raise ValueError(f"{script['src']} failed subresource integrity check {script['integrity']} (got {base64.b64encode(h.digest())})")
+			f.write(f"<script>\n/* {script['src']} */\n{data.decode('utf8')}\n</script>\n")
+	
+	return f.getvalue()
+
+config = configparser.ConfigParser()
+config.read('config.ini', encoding = "utf8")
+
+# tf_schema_overview.json is the WebAPI call to IEconItems_440/GetSchemaOverview
+with open('attributes.html', 'wt', encoding = 'utf8') as f:
+	f.write(chevron.render(template_string, {
+		**get_schema_overview(config['DEFAULT']),
+		'inline_scripts': inline_scripts,
+	}))

+ 205 - 0
tf_item_list.py

@@ -0,0 +1,205 @@
+#!/usr/bin/python3
+
+import base64
+import chevron
+import configparser
+import datetime
+import hashlib
+import html.parser
+import io
+import json
+import urllib
+import urllib.request
+import urllib.parse
+
+template_string = r'''
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<title>TF2 Item List</title>
+	
+	<style>
+	html { scroll-behavior: smooth; }
+	body {
+		font-family: Verdana,Arial,sans-serif;
+		font-size: 0.75em;
+		
+		background-color: #111;
+		color: #FFF;
+	}
+	td {
+		padding: 0.25em;
+	}
+	a {
+		color: #1eb;
+	}
+	.item {
+		vertical-align: top;
+	}
+	.header {
+		font-weight: bold;
+		position: sticky;
+		top: 0;
+		background-color: #111;
+	}
+	.footer {
+		margin-top: 2.5em;
+		text-align: center;
+	}
+	tr:nth-child(even) { background: #222; }
+	tr:hover { background: #333; }
+	
+	.item-list { width: 100%; }
+	
+	tr:target {
+		box-shadow: 0 0 1em .25em #abb;
+		position: relative;
+	}
+	.id a {
+		color: inherit;
+		text-decoration: none;
+		display: block;
+	}
+	
+	.q_0  { color: #B2B2B2; }
+	.q_3  { color: #476291; }
+	.q_5  { color: #8650AC; }
+	.q_6  { color: #FFD700; }
+	.q_11 { color: #CF6A32; }
+	.q_15 { color: #FAFAFA; }
+	</style>
+</head>
+<body>
+	<h1>List of Team Fortress 2 items:</h1>
+	
+	<p>Page generated for schema last modified {{update_time}}.  <a href="" download>Click here to download this page for offline viewing.</a></p>
+	
+	<table class="item-list">
+		<thead>
+			<tr class="item header">
+				<th class="id">ID</th>
+				<th class="name">Internal Name</th>
+				<th class="en_name">Localized Name</th>
+				<th class="slot">Item Slot</th>
+				<th class="cname">Entity</th>
+			</tr>
+		</thead>
+		<tbody>
+			{{#result.items}}
+			<tr class="item entry" id="{{defindex}}">
+				<td class="id"><a href="#{{defindex}}">{{defindex}}</a></td>
+				<td class="name q_{{item_quality}}">{{name}}</td>
+				<td>{{item_name}}</td>
+				<td>{{item_slot}}</td>
+				<td>{{item_class}}</td>
+			</tr>
+			{{/result.items}}
+		</tbody>
+	</table>
+	<div class="footer">
+		<div>
+			Generated with <a href="https://git.csrd.science/nosoop/py-tf2-schema-renderer">this script</a>.
+		</div>
+		<div>
+			Table sorting provided by <a href="https://github.com/tristen/tablesort">tablesort</a>.
+		</div>
+	</div>
+	
+	<!--
+	  - table sorting functionality
+	  - the page degrades gracefully without JS, but sorting is pretty nice to have
+	  -->
+	{{#inline_scripts}}
+	<script src="https://unpkg.com/tablesort@5.0.2/dist/tablesort.min.js"
+			integrity="sha384-MAoWp5qWRu0089eW0evm9gRng72sxH0daE2mkSXN8RoQ99hG/x+Wi8GxUbrgbYLp"
+			crossorigin="anonymous"></script>
+	<script src="https://unpkg.com/tablesort@5.0.2/dist/sorts/tablesort.number.min.js"
+			integrity="sha384-YPqBWGhq2JCwnOo6VsMYgE5XvUqQrzskaIGxc7CByfV6o9viRVq8uAhCs/0iPQBw"
+			crossorigin="anonymous"></script>
+	{{/inline_scripts}}
+	<script>
+		new Tablesort(document.querySelector('.item-list'));
+		window.onload = window.onhashchange = () => {
+			if (location.hash) {
+				document.getElementById(location.hash.slice(1)).scrollIntoView({
+					behavior: "auto", block: "center", inline: "center"
+				});
+			}
+		};
+	</script>
+</body>
+</html>
+'''
+
+def process_item_class(item):
+	used_by = item.get("used_by_classes", [])
+	if used_by:
+		item['used_by_classes'] = [ { "class": c.lower() } for c in used_by ]
+	return item
+
+def get_schema_items(params):
+	opts = dict(params.items())
+	opts['start'] = 0
+	
+	url = "https://api.steampowered.com/IEconItems_440/GetSchemaItems/v0001/"
+	while True:
+		with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(opts)}") as r:
+			data = json.load(r)['result']
+			
+			yield from data.get('items', [])
+			
+			if 'next' not in data:
+				break
+			opts['start'] = data['next']
+
+def inline_scripts(text, render):
+	"""
+	Helper function to parse script elements, download the scripts and inline them.
+	"""
+	class ScriptElementParser(html.parser.HTMLParser):
+		def __init__(self):
+			super(ScriptElementParser, self).__init__()
+			self.scripts = []
+			self.current = []
+		
+		def handle_starttag(self, tag, attrs):
+			if tag == 'script':
+				self.current.append(dict(attrs))
+		
+		def handle_endtag(self, tag):
+			self.scripts.append(self.current.pop())
+	
+	parser = ScriptElementParser()
+	parser.feed(text)
+	
+	f = io.StringIO()
+	for script in parser.scripts:
+		with urllib.request.urlopen(script['src']) as r:
+			data = r.read()
+			if 'integrity' in script:
+				ct, hash = script['integrity'].split('-', 1)
+				h = hashlib.new(ct)
+				h.update(data)
+				if base64.b64encode(h.digest()).decode('ascii') != hash:
+					raise ValueError(f"{script['src']} failed subresource integrity check {script['integrity']} (got {base64.b64encode(h.digest())})")
+			f.write(f"<script>\n/* {script['src']} */\n{data.decode('utf8')}\n</script>\n")
+	
+	return f.getvalue()
+
+def schema_mtime(params):
+	url = "https://api.steampowered.com/IEconItems_440/GetSchemaOverview/v0001/"
+	with urllib.request.urlopen(f"{url}?{urllib.parse.urlencode(dict(params.items()))}") as r:
+		return r.info().get('last-modified')
+
+config = configparser.ConfigParser()
+config.read('config.ini', encoding = "utf8")
+
+with open('items.html', 'wt', encoding = 'utf8') as f:
+	f.write(chevron.render(template_string, {
+		'result': {
+			'items': list(map(process_item_class, get_schema_items(config['DEFAULT'])))
+		},
+		'update_time': schema_mtime(config['DEFAULT']),
+		'inline_scripts': inline_scripts
+	}))