この投稿は「ごちうさ住民 Advent Calendar 2014」の9日目の記事です。
昨日はkazuheiさんの『私の所持金52円!!』でした。
雑コラメーカーのスマホ対応、心よりお待ちしております(チラッチラッ
ごちうさ1羽コメントスクリプトさん for RGSS3
概要
RPGツクールVX Ace(RGSS3)用のスクリプト素材です。
ゲーム画面にニコニコ動画の『ご注文はうさぎですか? 第1羽「ひと目で、尋常でないもふもふだと見抜いたよ」』のコメントを表示します。
利用規約
いつも通りテキトーにどうぞ。許諾も表記も何もナシでOKです。
ただし、画面に流れるコメントにはそれぞれ権利があるのでご注意ください。
なお、本スクリプトに関しては完全に一切のサポートを行いません。よろしくお願いします(ぺこり)
スクリプト
# coding: utf-8 #=============================================================================== # ■ ごちうさ1羽コメントスクリプトさん for RGSS3 #------------------------------------------------------------------------------- # 2014/12/09 Ru/むっくRu (@ru_shalm) #------------------------------------------------------------------------------- # ゲーム画面にニコニコ動画の # 『ご注文はうさぎですか? 第1羽「ひと目で、尋常でないもふもふだと見抜いたよ」』の # コメントを表示します。 #=============================================================================== module Torigoya module Base64 def self.encode(str) [str].pack('m*') end def self.decode(str) str.unpack('m*').join end end end module Torigoya module Win32 INTERNET_OPEN_TYPE_PRECONFIG = 0 INTERNET_SERVICE_HTTP = 3 def self.internet_open ::Win32API.new('wininet.dll', 'InternetOpen', %w(p l p p l), 'l') end def self.internet_connect ::Win32API.new('wininet.dll', 'InternetConnect', %w(p p l p p l l l), 'l') end def self.http_open_request ::Win32API.new('wininet.dll', 'HttpOpenRequest', %w(p p p p p p l l), 'l') end def self.http_send_request ::Win32API.new('wininet.dll', 'HttpSendRequest', %w(p p l p l), 'i') end def self.internet_read_file ::Win32API.new('wininet.dll', 'InternetReadFile', %w(l p l p), 'l') end def self.internet_close_handle ::Win32API.new('wininet.dll', 'InternetCloseHandle', %w(l), 'l') end end end module Torigoya class URI # URLエンコード # @param [String] str # @return [String] def self.encode(str) str.to_s.gsub(/([^A-Za-z0-9\-_\.!\~\*\'\(\);\/\?\:@&=\+\$,\[\]])/) do '%' + $1.unpack('H*').join('').scan(/.{2}/).join('%').upcase end end def self.parse(uri) self.new(uri) end def initialize(uri) /\A(?<scheme>[^:]+):\/\/(?<host>[^\/:]+)(?::(?<port>\d+))?(?<path>\/[^\?]*)?(?:\?(?<query>.*))?\z/ =~ uri @scheme = scheme.downcase @host = host @port = (port || default_port).to_i @path = path || '/' @query = query || '' end attr_reader :scheme attr_reader :host attr_reader :port attr_reader :path attr_reader :query private def default_port case self.scheme when 'http' 80 when 'https' 443 else raise 'unknown scheme' end end end module Http def self.client @client ||= Client.new end class Client SESSION_NAME = 'Torigoya RGSS' def get(uri, params = {}, headers = []) request('GET', uri, params, headers) end def post(uri, params = {}, headers = []) request('POST', uri, params, headers) end private def request(method, uri_str, params = {}, headers = []) uri = URI.parse(uri_str) payload = nil result_body = [] headers.push "Host: #{uri.host}" if params.kind_of?(String) path = uri.path payload = params else if method == 'GET' # TODO: uri.queryとdeep_mergeしましょう query = convert_query(params) path = "#{uri.path}?#{query}" else query = convert_query(params) path = "#{uri.path}#{uri.query ? "?#{uri.query}" : ''}" payload = params end end begin handle = Torigoya::Win32.internet_open.call(SESSION_NAME, Torigoya::Win32::INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0) http_session = Torigoya::Win32.internet_connect.call(handle, uri.host, uri.port, nil, nil, Torigoya::Win32::INTERNET_SERVICE_HTTP, 0, 0) http_request = Torigoya::Win32.http_open_request.call(http_session, method, path, nil, nil, nil, 0, 0) Torigoya::Win32.http_send_request.call(http_request, headers.join("\n"), -1, payload, (payload ? payload.size : 0)) while true buf = "\0" * 10240 read_size = "\0" * 4 # DWORD break if Torigoya::Win32.internet_read_file.call(http_request, buf, buf.size, read_size) && (size = read_size.unpack('i*')[0]) == 0 result_body.push buf[0, size] end rescue => e puts e.inspect puts $@ result_body = [] ensure Torigoya::Win32.internet_close_handle.call(http_request) if http_request Torigoya::Win32.internet_close_handle.call(http_session) if http_session Torigoya::Win32.internet_close_handle.call(handle) if handle end result_body.join end def convert_query(params = {}) params.to_a.map { |n| n.map { |n| URI.encode(n) }.join('=') }.join('&') end end end end module Torigoya module PoorXml # 超簡易XMLパーサー # - UTF-8以外読み込ませちゃダメ # - コメント(<!-- -->)を含むやつもダメ # - というかいろいろ読めない class Parser # 初期化 # @param [String] raw_doc XMLテキスト def initialize(raw_doc) @raw_doc = raw_doc end # パース処理 # @return [Node] XMLのrootノード def parse buffer = '' str = @raw_doc.dup stacks = [] root = Node.new('root') stacks.push root while (str.strip! || str.size > 0) case str when /^(<\?xml.+\?>)/ # XMLヘッダ(UTF-8以外読まないので無視) str.sub!($1, '') when /^(<([^>]+)\/>)/ # 単独タグ params = $2 str.sub!($1, '') child = line_to_node(params) stacks.last.children.push child when /^(<\/([^>]+)>)/ # 閉じタグ name = $2 str.sub!($1, '') stacks.pop when /^(<([^>]+)>)/ # 開始タグ params = $2 str.sub!($1, '') child = line_to_node(params) stacks.last.children.push child stacks.push child when /^([^<]+)</ text = $1 str.sub!($1, '') child = TextNode.new('text') child.text = text stacks.last.children.push child else raise 'invalid xml' end end root end private def line_to_node(line) name, attr_str = line.split(/\s+/, 2) node = Node.new(name) if attr_str i = 0 key = '' value = '' read_key_flag = true while i < attr_str.size if read_key_flag case attr_str[i] when /\s/ when '=' read_key_flag = false i += 1 # 次は「"」のはずなのでスキップ else key << attr_str[i] end else case attr_str[i] when "\\" i += 1 when '"' read_key_flag = true node.attributes[key] = value key = '' value = '' else value << attr_str[i] end end i += 1 end end node end end class Node # 初期化 # @param [String] name ノード名 def initialize(name) @name = name @attributes = {} @children = [] end attr_reader :name attr_reader :attributes attr_reader :children # ノードの子の中から指定の名を持つものを取り出す # @param [String] name 検索するノードの名前 # @return [Array] 配列 def [](name) self.children.select { |child| child.name == name } end # 属性の取得 # @param [String] name 属性名 # @return [Object] 指定属性の値 def attr(name) self.attributes[name] end # ノード内に含むテキストを返す # @return [String] 子ノードのテキストを全て連結した文字列 def text self.children.map(&:text).join('') end # Hash化する # @return [Hash] Hash化した値 def to_h { name: @name, attributes: @attributes, children: @children.map(&:to_h), } end end class TextNode < Node # 初期化 # @param [String] name ノード名 def initialize(name) super @text = '' end attr_accessor :text # Hash化する # @return [Hash] Hash化した値 def to_h super.tap { |o| o['text'] = @text } end end end end module Torigoya module NicoVideo class Comment def initialize(no, user_id, vpos, mail, text) @no = no @user_id = user_id @vpos = vpos @mail = mail @text = text end attr_reader :no attr_reader :user_id attr_reader :vpos attr_reader :mail attr_reader :text end class CommentLoader def initialize(thread_id, message_url) @thread_id = thread_id @message_url = message_url end def fetch fetch_comment_xml(fetch_threadkey) end def fetch_threadkey resp = Torigoya::Http.client.get( "http://flapi.nicovideo.jp/api/getthreadkey", thread: @thread_id ) response_set = {} resp.split('&').each do |n| set = n.split('=', 2) response_set[set[0].to_s] = set[1].to_s end response_set end def fetch_comment_xml(threadkey_data, size = 1000) thread_key = threadkey_data['threadkey'] force_184 = threadkey_data['force_184'] message_server_url = 'http://msg.nicovideo.jp/24/api/' data = "<thread res_from=\"-#{size}\" version=\"20061206\" thread=\"#{@thread_id}\" threadkey=\"#{thread_key}\" force_184=\"#{force_184}\" scores=\"1\" />" Torigoya::Http.client.post( message_server_url, data, ) end end end end module Torigoya class RabbitComment # ごちうさ1羽のコメントサーバ情報 # ログイン状態でgetflv API叩いて取得できるよ MESSAGE_SERVER_URL = 'http://msg.nicovideo.jp/24/api/' THREAD_ID = '1397552685' def self.load rc = RabbitComment.new rc.data['comments'] end def initialize @data = nil load_comment unless @data['fetched_at'] and @data['fetched_at'] > Time.now.to_i - (60 * 60 * 3) @data['fetched_at'] = Time.now.to_i @data['comments'] = fetch_comment File.open('tmp/comment.dat', 'wb') do |f| Marshal.dump(@data, f) end end end attr_reader :data def load_comment Dir.mkdir('tmp') unless File.exist?('tmp') if File.exist?('tmp/comment.dat') begin File.open('tmp/comment.dat', 'rb') do |file| @data = Marshal.load(file) end rescue => e puts e.inspect end end @data ||= {} end def fetch_comment nico_comment = Torigoya::NicoVideo::CommentLoader.new(THREAD_ID, MESSAGE_SERVER_URL) parser = Torigoya::PoorXml::Parser.new(nico_comment.fetch) doc = parser.parse chat_elements = doc['packet'].first['chat'].map { |node| Torigoya::NicoVideo::Comment.new( node.attr('no').to_i, node.attr('user_id'), node.attr('vpos').to_i, (node.attr('mail') || '').split(/\s+/), node.text, ) }.sort { |a, b| a.vpos <=> b.vpos } end end end module Torigoya module CommentPlayer module Manager def self.init(comments) @stage = Spriteset_Stage.new(comments) end def self.stage @stage end end class Sprite_Comment < Sprite def initialize(sprite_set, viewport = nil) super(viewport) @sprite_set = sprite_set @test_bitmap = Bitmap.new(1, 1) @speed = 1 @life = -1 self.z = 255 end def dispose @test_bitmap.dispose super end def set(comment) unset @comment = comment self.bitmap.dispose if self.bitmap apply_font(@test_bitmap) lines = @comment.text.strip.split(/\r?\n/) rect = @test_bitmap.text_size(lines.max_by(&:size)) if self.ue? or self.shita? rect.width = Graphics.width unless rect.width < Graphics.width align = 1 else align = 0 end self.bitmap = Bitmap.new(rect.width, rect.height * lines.size) apply_font(self.bitmap) lines.each do |line| self.bitmap.draw_text(rect, line, align) rect.y += rect.height end @speed = 0 case when self.ue? self.x = (Graphics.width - self.bitmap.width) / 2 self.y = @sprite_set.find_ue_position(self) when self.shita? self.x = (Graphics.width - self.bitmap.width) / 2 self.y = @sprite_set.find_shita_position(self) - self.bitmap.height else self.x = Graphics.width @speed = (Graphics.width + self.bitmap.width) / (4.0 * Graphics.frame_rate) self.y = rand(Graphics.height - self.bitmap.height) end @life = Graphics.frame_rate * 4 end def apply_font(bitmap) bitmap.font.size = case when @comment.mail.include?('small') 24 when @comment.mail.include?('big') 48 else 32 end # TODO: 16進数指定もあるよ bitmap.font.color = case when @comment.mail.include?('red') Color.new(255, 0, 0) when @comment.mail.include?('pink') Color.new(255, 128, 128) when @comment.mail.include?('orange') Color.new(255, 192, 0) when @comment.mail.include?('yellow') Color.new(255, 255, 0) when @comment.mail.include?('green') Color.new(0, 255, 0) when @comment.mail.include?('cyan') Color.new(0, 255, 255) when @comment.mail.include?('blue') Color.new(0, 0, 255) when @comment.mail.include?('purple') Color.new(192, 0, 255) when @comment.mail.include?('black') Color.new(0, 0, 0) when @comment.mail.include?('white2'), @comment.mail.include?('niconicowhite') Color.new(204, 204, 153) when @comment.mail.include?('red2'), @comment.mail.include?('truered') Color.new(204, 0, 51) when @comment.mail.include?('pink2') Color.new(255, 51, 204) when @comment.mail.include?('orange2'), @comment.mail.include?('passionorange') Color.new(255, 102, 0) when @comment.mail.include?('yellow2'), @comment.mail.include?('madyellow') Color.new(153, 153, 0) when @comment.mail.include?('green2'), @comment.mail.include?('elementalgreen') Color.new(0, 204, 102) when @comment.mail.include?('cyan2') Color.new(0, 204, 204) when @comment.mail.include?('blue2'), @comment.mail.include?('marineblue') Color.new(51, 153, 255) when @comment.mail.include?('purple2'), @comment.mail.include?('nobleviolet') Color.new(102, 51, 204) when @comment.mail.include?('black2') Color.new(102, 102, 102) else Color.new(255, 255, 255) end end def unset @comment = nil @life = 0 self.bitmap.dispose if self.bitmap end def update if @comment self.x -= @speed @life -= 1 if @life <= 0 self.unset end end super end def living? @life > 0 end def ue? @comment && @comment.mail.include?('ue') end def shita? @comment && @comment.mail.include?('shita') end end class Spriteset_Stage SPRITE_SIZE = 50 def initialize(comments) @comments = comments @comment_index = 0 @timer = 0 @prev_frame_count = Graphics.frame_count @viewport = Viewport.new @viewport.z = 65535 @sprites = Array.new(SPRITE_SIZE).map { Sprite_Comment.new(self, @viewport) } @sprite_index = 0 end def dispose @sprites.each do |sprite| sprite.bitmap.dispose if sprite.bitmap sprite.dispose end @viewport.dispose end def update # 時間を進める prev_timer = @timer @timer += ((Graphics.frame_count - @prev_frame_count).to_f / Graphics.frame_rate) @prev_frame_count = Graphics.frame_count # 経過時間中に登場するコメントを流す while @comments[@comment_index] && @comments[@comment_index].vpos <= (@timer * 100) @sprites[@sprite_index].set(@comments[@comment_index]) @sprite_index = (@sprite_index + 1) % SPRITE_SIZE @comment_index += 1 unless @comment_index < @comments.size @comment_index = 0 @timer = 0 end end @viewport.update @sprites.each(&:update) end # ue表示の表示位置を探して返す def find_ue_position(target_sprite) y = 0 @sprites.select(&:living?).select(&:ue?).sort { |a, b| a.y <=> b.y }.each do |sprite| return y if y + target_sprite.bitmap.height < sprite.y y = sprite.y + sprite.bitmap.height end y end # shita表示の表示位置を探して返す def find_shita_position(target_sprite) y = Graphics.height @sprites.select(&:living?).select(&:shita?).sort { |a, b| b.y <=> a.y }.each do |sprite| return y if y - target_sprite.bitmap.height > sprite.y + sprite.bitmap.height y = sprite.y end y end end end end class << DataManager alias gochiusa_init init def init gochiusa_init comments = Torigoya::RabbitComment.load Torigoya::CommentPlayer::Manager.init(comments) end end class Scene_Base alias gochiusa_update_basic update_basic def update_basic gochiusa_update_basic Torigoya::CommentPlayer::Manager.stage.update end end
一見、めっちゃ長いんですけど、RGSSからnet/httpとrexmlが使えないのが悪いんです…!XMLパーサー書くとか昭和かよ。
注意
- コメント表示の再現度が絶望的にダメです
- ニコ動のコメントシステム複雑すぎてつらいので誰かまとめてほしい
- コメントの取得数が足りません(直近1000件しか取ってきてない)
- よくコメント表示レイヤーが消失します
- シーンをまたがったSprite大量に置くのよくないと思う
間違ってもマジメなゲームに使ってはダメです
ごちうさの話
当時、タイトルの「うさぎ」に釣られて1羽を視聴しました。
「なるほどな」という感じだったので本来ならそのまま視聴継続になるはずだったのですが、4月当時、僕は完全に炎上案件の真っ只中で、仕事と睡眠を繰り返す日々で気づいたら夏になっており、僕の中でごちうさは1羽が最終回になってしまいました。
あのとき、リアルタイムに視聴していたら僕もどうなっていたかわかりませんね。。。
しかし、幸か不幸かはわかりませんが、弟がごちうさ民になっており、現在では家に原作もBlue-rayもある状態のため、ごちうさ分に関しては特に問題がない状態になっています。なので原作は3巻まで読みました。あと一応、電子版も買ったです。
シャロちゃんとリゼ先輩の関係がとても良いですね。
あの2人の関係は見てるだけでこう…グッとね。はい。
明日はゆっくりしないさんです٩(๑❛ᴗ❛๑)۶