この投稿は「ごちうさ住民 Advent Calendar 2014」の9日目の記事です。
昨日はkazuheiさんの『私の所持金52円!!』でした。
雑コラメーカーのスマホ対応、心よりお待ちしております(チラッチラッ
ごちうさ1羽コメントスクリプトさん for RGSS3
概要
RPGツクールVX Ace(RGSS3)用のスクリプト素材です。
ゲーム画面にニコニコ動画の『ご注文はうさぎですか? 第1羽「ひと目で、尋常でないもふもふだと見抜いたよ」』のコメントを表示します。
利用規約
いつも通りテキトーにどうぞ。許諾も表記も何もナシでOKです。
ただし、画面に流れるコメントにはそれぞれ権利があるのでご注意ください。
なお、本スクリプトに関しては完全に一切のサポートを行いません。よろしくお願いします(ぺこり)
スクリプト
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
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
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
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
class Parser
def initialize(raw_doc)
@raw_doc = raw_doc
end
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.+\?>)/
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
def initialize(name)
@name = name
@attributes = {}
@children = []
end
attr_reader :name
attr_reader :attributes
attr_reader :children
def [](name)
self.children.select { |child| child.name == name }
end
def attr(name)
self.attributes[name]
end
def text
self.children.map(&:text).join('')
end
def to_h
{
name: @name,
attributes: @attributes,
children: @children.map(&:to_h),
}
end
end
class TextNode < Node
def initialize(name)
super
@text = ''
end
attr_accessor :text
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
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
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
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
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人の関係は見てるだけでこう…グッとね。はい。
明日はゆっくりしないさんです٩(๑❛ᴗ❛๑)۶