You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

259 lines
6.4KB

  1. #!/usr/bin/env ruby
  2. require 'parallel'
  3. require 'zip'
  4. require 'oj'
  5. require 'sqlite3'
  6. require 'time'
  7. require 'erb'
  8. require 'fileutils'
  9. require './config'
  10. Zip.setup do |c|
  11. c.default_compression = Zlib::BEST_COMPRESSION
  12. c.continue_on_exists_proc = true
  13. end
  14. class Hash
  15. def leaves(&block)
  16. Enumerator.new do |e|
  17. self.class.leaves self, [], e, &block
  18. end
  19. end
  20. def symbolize_keys
  21. self.transform_keys { |k| k.to_sym rescue k }
  22. end
  23. def walk(&block)
  24. self.class._walk self, [], &block
  25. end
  26. private
  27. def self._walk(hash, path, &block)
  28. hash.each do |k, v|
  29. p = [*path, k]
  30. block.call :node, p, v
  31. case v
  32. when Hash
  33. _walk v, p, &block
  34. else
  35. block.call :leaf, p, v
  36. end
  37. end
  38. end
  39. def self.leaves(hash, path, enumerator, &block)
  40. hash.each do |k, v|
  41. p = [*path, k]
  42. case v
  43. when Hash
  44. self.leaves v, p, enumerator, &block
  45. else
  46. result = block.call p, v
  47. enumerator << result
  48. end
  49. end
  50. end
  51. end
  52. DURATION = {
  53. 'h' => 60*60*1_000,
  54. 'm' => 60*1_000,
  55. 's' => 1_000,
  56. 'ms' => 1,
  57. }.freeze
  58. def parse_duration(string)
  59. string.split
  60. .collect do |s|
  61. duration, type = s.match(/(\d+)(.+)/).captures
  62. duration = DURATION.fetch(type) * duration.to_i
  63. end
  64. .sum
  65. end
  66. def duration_to_s(duration)
  67. text = []
  68. DURATION.each do |k, v|
  69. part = duration / v
  70. text << "#{part}#{k}" if part > 0
  71. duration -= part * v
  72. end
  73. text.join '&nbsp;'
  74. end
  75. class Reports
  76. def initialize
  77. @reports = Hash.new { |h, k| h[k] = [] }
  78. end
  79. def <<(report)
  80. id = report.fetch :id
  81. @reports[id] << report
  82. end
  83. def [](boss)
  84. id = IDS[boss]
  85. return nil unless id && @reports.has_key?(id)
  86. reports = @reports.fetch(id).sort { |a, b| b.fetch(:date) <=> a.fetch(:date) }
  87. cm, standard = reports.partition { |r| r.fetch :cm }
  88. { cm: cm, standard: standard }
  89. end
  90. end
  91. def compress_evtc(evtc)
  92. puts evtc
  93. basename = File.basename evtc, '.evtc'
  94. dirname = File.dirname evtc
  95. zevtc = File.join dirname, basename + '.zevtc'
  96. FileUtils.rm_f zevtc
  97. Zip::File.open zevtc, Zip::File::CREATE do |zip|
  98. zip.add evtc, evtc
  99. end
  100. FileUtils.rm evtc
  101. zevtc
  102. end
  103. def process_evtcs
  104. Parallel.map(Dir['arcdps.cbtlogs/**/*.evtc'].sort) { |evtc| compress_evtc evtc }
  105. end
  106. def process_zevtcs(zevtcs)
  107. return if zevtcs.empty?
  108. system 'mono', 'GW2EI/GuildWars2EliteInsights.exe', '-p', '-c', 'gw2ei.conf', *zevtcs
  109. zevtcs.collect { |f| File.join 'html', File.basename(f, '.zevtc') + '.json.gz' }
  110. end
  111. def reprocess_zevtcs
  112. process_zevtcs Dir['arcdps.cbtlogs/**/*.zevtc'].sort
  113. end
  114. EXTRACT_BOSS_FROM_FILENAME = /\d{8}-\d{6}_([^_]+)_\d+s_(kill|fail).html/.freeze
  115. # BOSS_DIRECTORIES = BOSSES.leaves { |p, bs| d = p.first; bs.collect { |b| [b, d] } }
  116. # .to_a.flatten(1).to_h
  117. # .merge({ai: :fractals}).freeze
  118. BOSS_DIRECTORIES = ARCDPS_BOSSES.collect { |t, bs| bs.collect { |b| [b, t] } }
  119. .flatten(1).to_h.freeze
  120. def sort_html
  121. Dir['html/*.html'].sort.each do |file|
  122. basename = File.basename file
  123. match = EXTRACT_BOSS_FROM_FILENAME.match file
  124. next unless match
  125. boss = match[1].to_sym
  126. directory = BOSS_DIRECTORIES.fetch(boss).to_s
  127. directory = File.join 'html', directory
  128. type = match[2].to_sym
  129. directory = File.join directory, 'fail' if type == :fail
  130. FileUtils.mv file, directory
  131. end
  132. end
  133. def do_in_database
  134. SQLite3::Database.new 'reports.db' do |db|
  135. db.execute <<-SQL
  136. CREATE TABLE IF NOT EXISTS reports (
  137. filename TEXT NOT NULL,
  138. id INTEGER NOT NULL,
  139. cm BOOLEAN NOT NULL,
  140. date DATETIME NOT NULL,
  141. duration INTEGER NOT NULL,
  142. name TEXT NOT NULL,
  143. profession TEXT NOT NULL,
  144. total_dps INTEGER NOT NULL,
  145. top_dps INTEGER NOT NULL,
  146. own_dps INTEGER NOT NULL
  147. )
  148. SQL
  149. db.execute <<-SQL
  150. CREATE UNIQUE INDEX IF NOT EXISTS filename_index ON reports(filename)
  151. SQL
  152. db.execute <<-SQL
  153. CREATE INDEX IF NOT EXISTS id_index ON reports(id)
  154. SQL
  155. db.execute <<-SQL
  156. CREATE INDEX IF NOT EXISTS date_index ON reports(id, date DESC)
  157. SQL
  158. db.results_as_hash = true
  159. yield db
  160. end
  161. end
  162. def parse_json(file)
  163. File.open file do |fd|
  164. json = Oj.load Zlib::GzipReader.wrap fd
  165. id = json.fetch 'triggerID'
  166. name = json.fetch 'fightName'
  167. cm = json.fetch 'isCM'
  168. date = Time.parse json.fetch 'timeStartStd'
  169. duration = parse_duration json.fetch 'duration'
  170. players = json.fetch 'players'
  171. dps = players.collect { |p| p.fetch('dpsTargets').first.first.fetch('dps') }
  172. total_dps = dps.sum
  173. top_dps = dps.max
  174. me = players.find { |p| p.fetch('account') == ME }
  175. name = me.fetch 'name'
  176. own_dps = me.fetch('dpsTargets').first.first.fetch 'dps'
  177. profession = me.fetch 'profession'
  178. {
  179. id: id, cm: cm, date: date, duration: duration,
  180. name: name, profession: profession,
  181. total_dps: total_dps, top_dps: top_dps, own_dps: own_dps,
  182. }
  183. end
  184. end
  185. def process_json(db, json)
  186. # ap json
  187. filename = File.basename json, '.json.gz'
  188. row = db.get_first_row 'SELECT * FROM reports WHERE filename = ?', filename
  189. return if row
  190. report = parse_json json
  191. sql = <<-SQL
  192. INSERT INTO reports ( filename, id, cm, date, duration, name, profession, total_dps, top_dps, own_dps )
  193. VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
  194. SQL
  195. db.execute sql, filename, report.fetch(:id), (report.fetch(:cm) ? 1 : 0),
  196. report.fetch(:date).to_i, report.fetch(:duration), report.fetch(:name),
  197. report.fetch(:profession), report.fetch(:total_dps), report.fetch(:top_dps),
  198. report.fetch(:own_dps)
  199. end
  200. def process_jsons(db, jsons)
  201. Parallel.each jsons, in_threads: 16 do |json|
  202. process_json db, json
  203. end
  204. end
  205. def reprocess_jsons(db)
  206. process_jsons db, Dir["html/*_kill.json.gz"].sort
  207. end
  208. def generate_html(db)
  209. @reports = Reports.new
  210. rows = db.execute 'SELECT * FROM reports'
  211. rows.each do |row|
  212. row = row.symbolize_keys
  213. row[:cm] = row.fetch(:cm) == 1
  214. row[:date] = Time.at row.fetch :date
  215. @reports << row
  216. end
  217. erb = ERB.new File.read 'index.html.erb'
  218. html = erb.result binding
  219. File.write 'html/index.html', html
  220. end
  221. zevtcs = process_evtcs
  222. process_zevtcs zevtcs
  223. sort_html
  224. # # reprocess_zevtcs
  225. do_in_database do |db|
  226. reprocess_jsons db
  227. generate_html db
  228. end