Merge pull request #65 from Terreii/network-objects

All tests pass
This commit is contained in:
Christopher
2018-05-08 17:37:42 +02:00
committed by GitHub
11 changed files with 413 additions and 363 deletions
+180 -75
View File
@@ -1,5 +1,13 @@
/*
* Sends a message to the server.
* Every local chat and IM related action
*/
import {getValueOf, getStringValueOf} from '../network/msgGetters'
/*
*
* Sending Messages
*
*/
export function sendLocalChatMessage (text, type, channel) {
@@ -26,7 +34,7 @@ export function sendLocalChatMessage (text, type, channel) {
}
export function sendInstantMessage (text, to, id) {
return (dispatch, getState, {hoodie, circuit}) => {
return async (dispatch, getState, {hoodie, circuit}) => {
try {
const activeState = getState()
const session = activeState.session
@@ -88,100 +96,137 @@ export function sendInstantMessage (text, to, id) {
type: 'SelfSendImprovedInstantMessage',
msg
}
if (activeState.account.getIn(['viewerAccount', 'loggedIn'])) {
hoodie.store.add(msg).then(doc => {
dispatch(actionData)
})
} else {
dispatch(actionData)
if (shouldSaveChat(activeState)) {
await hoodie.store.add(msg)
}
dispatch(actionData)
} catch (e) {
console.error(e)
}
}
}
/*
*
* Receiving messages
*
*/
export function receiveChatFromSimulator (msg) {
const chatMsg = {
fromName: getStringValueOf(msg, 'ChatData', 'FromName'),
sourceID: getValueOf(msg, 'ChatData', 'SourceID'),
ownerID: getValueOf(msg, 'ChatData', 'OwnerID'),
sourceType: getValueOf(msg, 'ChatData', 'SourceType'),
chatType: getValueOf(msg, 'ChatData', 'ChatType'),
audible: getValueOf(msg, 'ChatData', 'Audible'),
position: getValueOf(msg, 'ChatData', 'Position'),
message: getStringValueOf(msg, 'ChatData', 'Message'),
time: Date.now()
}
return dispatchChatAction(msg.name, chatMsg, 'localchat/' + new Date(chatMsg.time).toJSON())
}
export function receiveIM (message) {
return async dispatch => {
const toAgentID = getValueOf(message, 'MessageBlock', 'ToAgentID')
const fromId = getValueOf(message, 'AgentData', 'AgentID')
const time = getValueOf(message, 'MessageBlock', 'Timestamp')
const dialog = getValueOf(message, 'MessageBlock', 'Dialog')
const fromAgentName = getStringValueOf(message, 'MessageBlock', 'FromAgentName')
const IMmsg = {
sessionID: getValueOf(message, 'AgentData', 'SessionID'),
fromId,
fromGroup: getValueOf(message, 'MessageBlock', 'FromGroup'),
toAgentID,
parentEstateID: getValueOf(message, 'MessageBlock', 'ParentEstateID'),
regionID: getValueOf(message, 'MessageBlock', 'RegionID'),
position: getValueOf(message, 'MessageBlock', 'Position'),
offline: getValueOf(message, 'MessageBlock', 'Offline'),
dialog,
id: getValueOf(message, 'MessageBlock', 'ID'),
fromAgentName,
message: getStringValueOf(message, 'MessageBlock', 'Message'),
binaryBucket: getValueOf(message, 'MessageBlock', 'BinaryBucket'),
time: time !== 0 ? time * 1000 : Date.now()
}
// If it is a group chat, toAgentID is the Group-UUID.
IMmsg.chatUUID = IMmsg.fromGroup ? IMmsg.toAgentID : IMmsg.id
// Start a new IMChat.
await dispatch(createNewIMChat(dialog, IMmsg.chatUUID, fromId, fromAgentName))
const id = `imChats/${IMmsg.chatUUID}/${new Date(IMmsg.time).toJSON()}`
dispatch(dispatchChatAction(message.name, IMmsg, id))
}
}
// Dispatches chat (and IM) messages.
// They will be saved and synced under the avatar name.
function dispatchChatAction (name, msg, id) {
return async (dispatch, getState, {hoodie}) => {
const activeState = getState()
if (shouldSaveChat(activeState)) {
// Save messages. They will also be synced!
msg._id = activeState.account.get('avatarIdentifier') + '/' + id
const doc = await hoodie.store.add(msg)
dispatch({
type: name,
msg: doc
})
} else {
// This is the path for every message, that will not be synced and saved.
dispatch({
type: name,
msg
})
}
}
}
/*
*
* Start a new (IM) Chat and load the history
*
*/
export function getLocalChatHistory (avatarIdentifier) {
return (dispatch, getState, {hoodie}) => {
return hoodie.store.withIdPrefix(`${avatarIdentifier}/localchat/`).findAll()
}
}
// Get the chatType stored in an IMChat Info from the dialog value in IMs.
export function getIMChatTypeOfDialog (dialog) {
switch (dialog) {
case 0:
return 'personal'
default:
return undefined
}
}
// UUID make structure: 00000000-0000-4000-x000-000000000000
// all are hexadecimal numbers.
// 4 is always 4 and x is between 8 and b, but only if correct == true
// XOR of IM-chats isn't a correct UUID.
function uuidXOR (idIn1, idIn2, correct = false) {
const id1 = idIn1.toString().replace(/-/gi, '')
const id2 = idIn2.toString().replace(/-/gi, '')
let out = ''
for (let i = 0; i < 16; ++i) {
const index = i * 2
const byte1 = parseInt(id1[index] + id1[index + 1], 16)
const byte2 = parseInt(id2[index] + id2[index + 1], 16)
let xorByte = byte1 ^ byte2
if (correct && i === 6) { // Makes the 4 in the UUID
xorByte = (0b00001111 & xorByte) | (4 << 4)
} else if (correct && i === 8) { // Makes the y in the UUID. It is between 8 and b
xorByte = (8 << 4) + (0b00111111 & xorByte)
}
if (i === 4 || i === 6 || i === 8 || i === 10) {
out += '-'
}
out += xorByte.toString(16).padStart(2, '0')
}
return out
}
// Create a new chatUUID from type, target-UUID & agentUUID
function calcChatUUID (type, targetId, agentId) {
if (type === 'personal') {
return uuidXOR(agentId, targetId)
} else {
throw new Error(`Chat type '${type}' not jet supported!`)
}
}
// Start a new IM Chat from the UI.
export function startNewIMChat (dialog, targetId, name) {
return (dispatch, getState, {hoodie}) => {
try {
const chatType = getIMChatTypeOfDialog(dialog)
const chatUUID = calcChatUUID(chatType, targetId, getState().account.get('agentId'))
if (chatType === 'personal') {
try {
name = getState().names.getIn(['names', targetId.toString()]).getName()
} catch (error) {
console.error(error)
}
return async (dispatch, getState, {hoodie}) => {
const chatType = getIMChatTypeOfDialog(dialog)
const chatUUID = calcChatUUID(chatType, targetId, getState().account.get('agentId'))
if (chatType === 'personal') {
try {
name = getState().names.getIn(['names', targetId.toString()]).getName()
} catch (error) {
console.error(error)
}
dispatch(createNewIMChat(dialog, chatUUID, targetId, name))
return Promise.resolve(chatUUID)
} catch (error) {
return Promise.reject(error)
}
await dispatch(createNewIMChat(dialog, chatUUID, targetId, name))
return chatUUID
}
}
// Starts a new IMChat. It also saves it into Hoodie.
export function createNewIMChat (dialog, chatUUID, target, name) {
function createNewIMChat (dialog, chatUUID, target, name) {
const type = getIMChatTypeOfDialog(dialog)
if (type == null) return () => {}
return (dispatch, getState, {hoodie}) => {
const activeState = getState()
const hasChat = activeState.IMs.has(chatUUID)
@@ -197,7 +242,7 @@ export function createNewIMChat (dialog, chatUUID, target, name) {
})
// If the user is logged in with a viewer-account, then save the IMChat.
if (hasChat || !activeState.account.getIn(['viewerAccount', 'loggedIn'])) return
if (hasChat || !shouldSaveChat(activeState)) return
const avatarIdentifier = activeState.account.get('avatarIdentifier')
const doc = {
_id: `${avatarIdentifier}/imChatsInfos/${chatUUID}`,
@@ -206,7 +251,7 @@ export function createNewIMChat (dialog, chatUUID, target, name) {
target,
name
}
hoodie.store.updateOrAdd(doc)
return hoodie.store.findOrAdd(doc)
}
}
@@ -249,3 +294,63 @@ export function getIMHistory (chatUUID) {
})
}
}
/*
*
* Helper functions
*
*/
// checks if the chat history should be saved and synced
function shouldSaveChat (activeState) {
return activeState.account.getIn(['viewerAccount', 'loggedIn']) &&
activeState.account.get('sync')
}
// UUID make structure: 00000000-0000-4000-x000-000000000000
// all are hexadecimal numbers.
// 4 is always 4 and x is between 8 and b, but only if correct == true
// XOR of IM-chats isn't a correct UUID.
function uuidXOR (idIn1, idIn2, correct = false) {
const id1 = idIn1.toString().replace(/-/gi, '')
const id2 = idIn2.toString().replace(/-/gi, '')
let out = ''
for (let i = 0; i < 16; ++i) {
const index = i * 2
const byte1 = parseInt(id1[index] + id1[index + 1], 16)
const byte2 = parseInt(id2[index] + id2[index + 1], 16)
let xorByte = byte1 ^ byte2
if (correct && i === 6) { // Makes the 4 in the UUID
xorByte = (0b00001111 & xorByte) | (4 << 4)
} else if (correct && i === 8) { // Makes the x in the UUID. It is between 8 and b
xorByte = (8 << 4) + (0b00111111 & xorByte)
}
if (i === 4 || i === 6 || i === 8 || i === 10) {
out += '-'
}
out += xorByte.toString(16).padStart(2, '0')
}
return out
}
// Create a new chatUUID from type, target-UUID & agentUUID
function calcChatUUID (type, targetId, agentId) {
if (type === 'personal') {
return uuidXOR(agentId, targetId)
} else {
throw new Error(`Chat type '${type}' not jet supported!`)
}
}
// Get the chatType stored in an IMChat Info from the dialog value in IMs.
export function getIMChatTypeOfDialog (dialog) {
switch (dialog) {
case 0:
return 'personal'
default:
return undefined
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import createCallback from './simAction'
// Starts listening to packets on the circuit and dispatch an parsed action.
// Starts listening to packets on the circuit and dispatch a parsed action.
export default function init () {
return (dispatch, getState, {circuit}) => {
let callback = createCallback(dispatch)
+38 -127
View File
@@ -1,151 +1,62 @@
import { createNewIMChat } from './chatMessageActions'
import {receiveChatFromSimulator, receiveIM} from './chatMessageActions'
import {getValueOf, mapBlockOf} from '../network/msgGetters'
function parseChatFromSimulator (msg) {
const chatMsg = {
fromName: msg.getStringValue('ChatData', 'FromName'),
sourceID: msg.getValue('ChatData', 'SourceID'),
ownerID: msg.getValue('ChatData', 'OwnerID'),
sourceType: msg.getValue('ChatData', 'SourceType'),
chatType: msg.getValue('ChatData', 'ChatType'),
audible: msg.getValue('ChatData', 'Audible'),
position: msg.getValue('ChatData', 'Position'),
message: msg.getStringValue('ChatData', 'Message'),
time: Date.now()
}
return chatMsg
}
function parseIM (message) {
const toAgentID = message.getValue('MessageBlock', 'ToAgentID')
const fromId = message.getValue('AgentData', 'AgentID')
const time = message.getValue('MessageBlock', 'Timestamp')
const IMmsg = {
sessionID: message.getValue('AgentData', 'SessionID'),
fromId,
fromGroup: message.getValue('MessageBlock', 'FromGroup'),
toAgentID,
parentEstateID: message.getValue('MessageBlock', 'ParentEstateID'),
regionID: message.getValue('MessageBlock', 'RegionID'),
position: message.getValue('MessageBlock', 'Position'),
offline: message.getValue('MessageBlock', 'Offline'),
dialog: message.getValue('MessageBlock', 'Dialog'),
id: message.getValue('MessageBlock', 'ID'),
fromAgentName: message.getStringValue('MessageBlock', 'FromAgentName'),
message: message.getStringValue('MessageBlock', 'Message'),
binaryBucket: message.getValue('MessageBlock', 'BinaryBucket'),
time: time !== 0 ? time : Date.now()
}
// If it is a group chat, toAgentID is the Group-UUID.
IMmsg.chatUUID = IMmsg.fromGroup ? IMmsg.toAgentID : IMmsg.id
return IMmsg
}
function parseUUIDNameReply (message) {
return message.mapBlock('UUIDNameBlock', getValue => {
return {
firstName: getValue('FirstName', true),
lastName: getValue('LastName', true),
id: getValue('ID')
}
})
}
function parseUserRights (message, getState) {
const rights = message.mapBlock('Rights', getValue => {
return {
agentId: getValue('AgentRelated'),
rights: getValue('RelatedRights')
}
})
return {
ownId: getState().account.get('agentId'),
fromId: message.getValue('AgentData', 'AgentID'),
userRights: rights
}
}
function parseRegionInfo (message) {
return {
regionInfo: message.getValues('RegionInfo', 0, []),
regionInfo2: message.getValues('RegionInfo2', 0, [])
}
}
// Gets all messages from the SIM and filters them for the UI
// Gets all messages from the SIM and filters them, and if needed: calls their own actions.
function simActionFilter (msg) {
const name = msg.name
switch (name) {
switch (msg.name) {
case 'ChatFromSimulator':
const parsed = parseChatFromSimulator(msg)
return dispatchSIMAction(name, parsed, 'localchat/' + new Date(parsed.time).toJSON())
return receiveChatFromSimulator(msg)
case 'ImprovedInstantMessage':
const parsedMsg = parseIM(msg)
// Start a new IMChat.
return dispatch => {
dispatch(createNewIMChat(
parsedMsg.dialog, parsedMsg.chatUUID, parsedMsg.fromId, parsedMsg.fromAgentName
))
const id = `imChats/${parsedMsg.chatUUID}/${new Date(parsedMsg.time).toJSON()}`
dispatch(dispatchSIMAction(name, parsedMsg, id))
}
case 'UUIDNameReply':
return dispatchSIMAction(name, parseUUIDNameReply(msg))
return receiveIM(msg)
case 'ChangeUserRights':
return (dispatch, getState) => {
dispatch(dispatchSIMAction(name, parseUserRights(msg, getState)))
}
case 'AgentMovementComplete':
return dispatchSIMAction(name, {
position: msg.getValue('Data', 'Position'),
lookAt: msg.getValue('Data', 'LookAt')
})
case 'RegionInfo':
return dispatchSIMAction(name, parseRegionInfo(msg))
return parseUserRights(msg)
case 'RegionHandshake':
return sendRegionHandshakeReply(msg)
// For all messages that will and can be directly dispatched
case 'AgentMovementComplete':
case 'RegionInfo':
case 'UUIDNameReply':
return msg
default:
if (process.env.NODE_ENV !== 'production' && window.debugDispatchAllMsg) {
return msg
}
break
}
}
// Dispatches all parsed messages.
// If they have an ID, they will be saved and synced under the avatar name.
function dispatchSIMAction (name, msg, id) {
return (dispatch, getState, {hoodie}) => {
const activeState = getState()
if (typeof id === 'string' && activeState.account.getIn(['viewerAccount', 'loggedIn'])) {
// Save messages. They will also be synced!
const avatarIdentifier = activeState.account.get('avatarIdentifier')
msg._id = avatarIdentifier + '/' + id
hoodie.store.add(msg).then(doc => {
dispatch({
type: name,
msg: doc
})
})
} else {
// This is the path for every message, that will not be synced and saved.
dispatch({
type: name,
msg
})
}
// A global variable for development.
// Set it in development to true to dispatch all network messages.
if (process.env.NODE_ENV !== 'production') {
window.debugDispatchAllMsg = window.debugDispatchAllMsg || false
}
function parseUserRights (message) {
return (dispatch, getState) => {
const rights = mapBlockOf(message, 'Rights', getValue => {
return {
agentId: getValue('AgentRelated'),
rights: getValue('RelatedRights')
}
})
dispatch({
type: 'ChangeUserRights',
ownId: getState().account.get('agentId'),
fromId: getValueOf(message, 'AgentData', 'AgentID'),
userRights: rights
})
}
}
function sendRegionHandshakeReply (RegionHandshake) {
return (dispatch, getState, {circuit}) => {
const regionID = RegionHandshake.getValue('RegionInfo2', 'RegionID')
const flags = RegionHandshake.getValue('RegionInfo', 'RegionFlags')
const regionID = getValueOf(RegionHandshake, 'RegionInfo2', 'RegionID')
const flags = getValueOf(RegionHandshake, 'RegionInfo', 'RegionFlags')
const session = getState().session
+4 -3
View File
@@ -8,7 +8,8 @@
import events from 'events'
import { parseBody, createBody } from './networkMessages'
import {parseBody, createBody} from './networkMessages'
import {getValueOf, mapBlockOf} from './msgGetters'
export default class Circuit extends events.EventEmitter {
constructor (hostIP, hostPort, circuitCode) {
@@ -95,13 +96,13 @@ export default class Circuit extends events.EventEmitter {
this.send('CompletePingCheck', {
PingID: [
{
PingID: parsedBody.getValue('PingID', 0, 'PingID')
PingID: getValueOf(parsedBody, 'PingID', 0, 'PingID')
}
]
})
return
} else if (parsedBody.name === 'PacketAck') {
this._filterViewerAcks(parsedBody.mapBlock('Packets', getValue => getValue('ID')))
this._filterViewerAcks(mapBlockOf(parsedBody, 'Packets', getValue => getValue('ID')))
return
}
this.emit(parsedBody.name, parsedBody)
+2 -1
View File
@@ -3,6 +3,7 @@
import Circuit from './circuit'
import {createBody, parseBody} from './networkMessages'
import {getValueOf} from './msgGetters'
// Utility for testing
window.WebSocket = class WebSocket {
@@ -180,7 +181,7 @@ test('circuit should send after 200ms a PacketAck', done => {
: parsedAckMessageB
expect(parsedAckMessage).toBeTruthy()
expect(parsedAckMessage.getValue('Packets', 0, 'ID')).toBe(0)
expect(getValueOf(parsedAckMessage, 'Packets', 0, 'ID')).toBe(0)
done()
}, 250)
+98
View File
@@ -0,0 +1,98 @@
// Helper functions to access values in network messages
// Return the value of a variable in an block
// msg.getValue(blockName, [blockIndex,] variableName)
// blockIndex defaults to 0
export function getValueOf (msg, blockName, blockOrValue, varName) {
let blockNumber = 0
let variableName
if (varName == null) {
variableName = blockOrValue
} else {
blockNumber = +blockOrValue
variableName = varName
}
return msg[blockName][blockNumber][variableName]
}
// Transforms the value of a variable into a string.
// If the value is a Buffer (Fixed, Variable1 or Variable2)
// then it will be parsed as a UTF-8 String.
export function getStringValueOf (msg, blockName, blockOrValue, varName) {
const value = getValueOf(msg, blockName, blockOrValue, varName)
return parseValueAsString(value)
}
// Return the value of multiple variables in an block
// getValueOf(msg, blockName, [blockIndex,] variableNames)
// blockIndex defaults to 0
export function getValuesOf (msg, blockName, blockOrValues, varNames) {
let blockNumber = 0
let variableNames
if (varNames == null) {
variableNames = blockOrValues
} else {
blockNumber = +blockOrValues
variableNames = varNames
}
if (!Array.isArray(variableNames)) throw new TypeError('names of variables must be an Array!')
const blockInstance = msg[blockName][blockNumber]
if (variableNames.length === 0) {
variableNames = Object.keys(blockInstance)
}
return variableNames.reduce((result, name) => {
result[name] = blockInstance[name]
return result
}, {})
}
// Returns multiple values as a object.
// Transforms the value of a variable into a string.
// If the value is a Buffer (Fixed, Variable1 or Variable2)
// then it will be parsed as a UTF-8 String.
export function getStringValuesOf (msg, blockName, blockOrValues, varNames) {
const values = getValuesOf(msg, blockName, blockOrValues, varNames)
return Object.keys(values).reduce((result, key) => {
result[key] = parseValueAsString(values[key])
return result
}, {})
}
// How many instances of a block are there?
export function getNumberOfBlockInstancesOf (msg, blockName) {
return msg[blockName].length
}
// Maps over every instance of a block.
// expects the block name and a function.
// The function receives a getValue function and the index.
// The getValue function expects the name of a variable
// and as an optional second argument a Boolean if the value should be a String.
export function mapBlockOf (msg, blockName, fn) {
return msg[blockName].map((blockInstance, index) => {
const getter = (valueName, asString = false) => {
const value = blockInstance[valueName]
return asString
? parseValueAsString(value)
: value
}
return fn(getter, index)
})
}
// Helper function to stringify
function parseValueAsString (value) {
return Buffer.isBuffer(value)
? value.toString('utf8').replace(/\0/gi, '')
: value.toString()
}
+37 -127
View File
@@ -171,7 +171,7 @@ export function parseBody (
throw new Error('no message of this type')
}
const body = new ReceivedMessage(
const body = createReceivedMessage(
messagesByFrequency[frequency][num],
packetBody.slice(offset),
ip,
@@ -230,13 +230,15 @@ function parseVariable (variableTemplate, buffer, offset) {
return value
}
// Class for all Buffer -> Message action (on socket in)
// function for all Buffer -> Message action (on socket in)
//
// {
// name: String,
// frequency: 'High'|'Medium'|'Low'|'Fixed',
// number: Number,
// trusted: Boolean,
// isReliable: Boolean,
// isResend: Boolean,
// zerocoded: Boolean,
// isOld: undefined|String,
// size: Number,
@@ -247,138 +249,46 @@ function parseVariable (variableTemplate, buffer, offset) {
// }
// ]
// }
export class ReceivedMessage {
constructor (template, buffer, ip = '0.0.0.0', port = 0, isResend = false, isReliable = false) {
if (typeof template === 'string') {
template = messagesByName[template]
}
this.name = template.name
this.type = 'UDP' + template.name // for directly dispatching to redux
this.trusted = template.trusted
this.isReliable = isReliable
this.isResend = isResend
// no need for decoding, was done in circuit
this.isOld = template.isOld
this.from = {
function createReceivedMessage (
template, buffer, ip = '0.0.0.0', port = 0, isResend = false, isReliable = false
) {
if (typeof template === 'string') {
template = messagesByName[template]
}
const msg = {
name: template.name,
type: 'UDP' + template.name, // for directly dispatching to redux
trusted: template.trusted,
isReliable,
isResend,
frequency: template.frequency,
number: template.number,
isOld: template.isOld,
from: {
ip,
port
}
},
// parse the blocks
const offset = {
value: 0
}
const blocks = template.body.map(blockTemplate => {
const block = parseBlock(blockTemplate, buffer, offset)
// that the block is accessible through the name
this[blockTemplate.name] = block
return block
})
this.blocks = blocks
this.size = offset.value // ??? or something other
blocks: {},
size: 0
}
get frequency () {
return messagesByName[this.name].frequency
// parse the blocks
const offset = {
value: 0
}
get number () {
return messagesByName[this.name].number
}
const blocks = template.body.map(blockTemplate => {
const block = parseBlock(blockTemplate, buffer, offset)
msg[blockTemplate.name] = block
return block
})
// Return the value of a variable in an block
// msg.getValue(blockName, [blockIndex,] variableName)
// blockIndex defaults to 0
getValue (blockName, blockOrValue, varName) {
let blockNumber = 0
let variableName
msg.blocks = blocks
msg.size = offset.value // ??? or something other
if (varName == null) {
variableName = blockOrValue
} else {
blockNumber = +blockOrValue
variableName = varName
}
return this[blockName][blockNumber][variableName]
}
// Transforms the value of a variable into a string.
// If the value is a Buffer (Fixed, Variable1 or Variable2)
// then it will be parsed as a UTF-8 String.
getStringValue (blockName, blockOrValue, varName) {
const value = this.getValue(blockName, blockOrValue, varName)
return this._parseValueAsString(value)
}
// Return the value of multiple variables in an block
// msg.getValue(blockName, [blockIndex,] variableNames)
// blockIndex defaults to 0
getValues (blockName, blockOrValues, varNames) {
let blockNumber = 0
let variableNames
if (varNames == null) {
variableNames = blockOrValues
} else {
blockNumber = +blockOrValues
variableNames = varNames
}
if (!Array.isArray(variableNames)) throw new TypeError('names of variables must be an Array!')
const blockInstance = this[blockName][blockNumber]
if (variableNames.length === 0) {
variableNames = Object.keys(blockInstance)
}
return variableNames.reduce((result, name) => {
result[name] = blockInstance[name]
return result
}, {})
}
// Returns multiple values as a object.
// Transforms the value of a variable into a string.
// If the value is a Buffer (Fixed, Variable1 or Variable2)
// then it will be parsed as a UTF-8 String.
getStringValues (blockName, blockOrValues, varNames) {
const values = this.getValues(blockName, blockOrValues, varNames)
return Object.keys(values).reduce((result, key) => {
result[key] = this._parseValueAsString(values[key])
return result
}, {})
}
_parseValueAsString (value) {
return Buffer.isBuffer(value)
? value.toString('utf8').replace(/\0/gi, '')
: value.toString()
}
// How many instances of a block are there?
getNumberOfBlockInstances (blockName) {
return this[blockName].length
}
// Maps over every instance of a block.
// expects the block name and a function.
// The function receives a getValue function and the index.
// The getValue function expects the name of a variable
// and as an optional second argument a Boolean if the value should be a String.
mapBlock (blockName, fn) {
return this[blockName].map((blockInstance, index) => {
const getter = (valueName, asString = false) => {
const value = blockInstance[valueName]
return asString
? this._parseValueAsString(value)
: value
}
return fn(getter, index)
})
}
return msg
}
+33 -25
View File
@@ -4,7 +4,15 @@
import uuid from 'uuid'
import { parseBody, createBody, ReceivedMessage } from './networkMessages'
import {parseBody, createBody} from './networkMessages'
import {
getValueOf,
getStringValueOf,
getValuesOf,
getStringValuesOf,
getNumberOfBlockInstancesOf,
mapBlockOf
} from './msgGetters'
describe('parseBody', () => {
const buffer = Buffer.alloc(4 + (4 * (1 + (4 * 3))) + 1)
@@ -15,7 +23,7 @@ describe('parseBody', () => {
const testMessage = parseBody(buffer, '127.0.0.1', 8080, true, true)
test('should return the TestMessage', () => {
expect(testMessage instanceof ReceivedMessage).toBe(true)
expect(testMessage.constructor === Object).toBe(true)
expect(testMessage.frequency).toBe('Low')
expect(testMessage.number).toBe(1)
expect(testMessage.name).toBe('TestMessage')
@@ -31,13 +39,13 @@ describe('parseBody', () => {
})
test('should have one U32 in an array in the TestBlock1', () => {
expect(testMessage.getNumberOfBlockInstances('TestBlock1')).toBe(1)
expect(getNumberOfBlockInstancesOf(testMessage, 'TestBlock1')).toBe(1)
expect(testMessage.getValue('TestBlock1', 0, 'Test1')).toBe(0)
expect(getValueOf(testMessage, 'TestBlock1', 0, 'Test1')).toBe(0)
})
test('should have 3 U32 in 4 Arrays in NeighborBlock', () => {
expect(testMessage.getNumberOfBlockInstances('NeighborBlock')).toBe(4)
expect(getNumberOfBlockInstancesOf(testMessage, 'NeighborBlock')).toBe(4)
const values = [
'Test0',
@@ -50,22 +58,22 @@ describe('parseBody', () => {
Test2: 0
}
expect(testMessage.getValues('NeighborBlock', 0, values)).toEqual(shouldValues)
expect(testMessage.getValues('NeighborBlock', 1, values)).toEqual(shouldValues)
expect(testMessage.getValues('NeighborBlock', 2, values)).toEqual(shouldValues)
expect(testMessage.getValues('NeighborBlock', 3, values)).toEqual(shouldValues)
expect(getValuesOf(testMessage, 'NeighborBlock', 0, values)).toEqual(shouldValues)
expect(getValuesOf(testMessage, 'NeighborBlock', 1, values)).toEqual(shouldValues)
expect(getValuesOf(testMessage, 'NeighborBlock', 2, values)).toEqual(shouldValues)
expect(getValuesOf(testMessage, 'NeighborBlock', 3, values)).toEqual(shouldValues)
})
test('should return Strings for values', () => {
expect(testMessage.getStringValue('TestBlock1', 'Test1')).toBe('0')
expect(testMessage.getStringValue('NeighborBlock', 0, 'Test0')).toBe('0')
expect(testMessage.getStringValue('NeighborBlock', 1, 'Test1')).toBe('0')
expect(testMessage.getStringValue('NeighborBlock', 2, 'Test2')).toBe('0')
expect(testMessage.getStringValue('NeighborBlock', 3, 'Test0')).toBe('0')
expect(getStringValueOf(testMessage, 'TestBlock1', 'Test1')).toBe('0')
expect(getStringValueOf(testMessage, 'NeighborBlock', 0, 'Test0')).toBe('0')
expect(getStringValueOf(testMessage, 'NeighborBlock', 1, 'Test1')).toBe('0')
expect(getStringValueOf(testMessage, 'NeighborBlock', 2, 'Test2')).toBe('0')
expect(getStringValueOf(testMessage, 'NeighborBlock', 3, 'Test0')).toBe('0')
})
test('should map over block instances', () => {
const data = testMessage.mapBlock('NeighborBlock', (getValue, index) => {
const data = mapBlockOf(testMessage, 'NeighborBlock', (getValue, index) => {
return `${index} Test0 ${getValue('Test0')}`
})
expect(data).toEqual([
@@ -78,9 +86,9 @@ describe('parseBody', () => {
})
test('should get multiple values', () => {
const data = testMessage.getValues('NeighborBlock', 1, ['Test0', 'Test1', 'Test2'])
const dataStr = testMessage.getStringValues('NeighborBlock', ['Test0', 'Test1', 'Test2'])
const data2 = testMessage.getValues('NeighborBlock', [])
const data = getValuesOf(testMessage, 'NeighborBlock', 1, ['Test0', 'Test1', 'Test2'])
const dataStr = getStringValuesOf(testMessage, 'NeighborBlock', ['Test0', 'Test1', 'Test2'])
const data2 = getValuesOf(testMessage, 'NeighborBlock', [])
expect(data).toEqual({
'Test0': 0,
@@ -198,8 +206,8 @@ describe('parseBody should work with buffer from createBody', () => {
const parsedMessage = parseBody(buffy.buffer)
expect(parsedMessage.name).toBe('TestMessage')
expect(parsedMessage.getValue('TestBlock1', 'Test1')).toBe(1337)
expect(parsedMessage.getValue('NeighborBlock', 3, 'Test2')).toBe(11)
expect(getValueOf(parsedMessage, 'TestBlock1', 'Test1')).toBe(1337)
expect(getValueOf(parsedMessage, 'NeighborBlock', 3, 'Test2')).toBe(11)
})
test('NeighborList', () => {
@@ -246,7 +254,7 @@ describe('parseBody should work with buffer from createBody', () => {
const parsedMessage = parseBody(buffy.buffer)
expect(parsedMessage.name).toBe('NeighborList')
expect(parsedMessage.getValues('NeighborBlock', 0, [
expect(getValuesOf(parsedMessage, 'NeighborBlock', 0, [
'IP',
'Port',
'RegionID',
@@ -257,7 +265,7 @@ describe('parseBody should work with buffer from createBody', () => {
RegionID: aUUID,
SimAccess: 13
})
expect(parsedMessage.getStringValue('NeighborBlock', 'Name')).toBe('Hello Sim!')
expect(getStringValueOf(parsedMessage, 'NeighborBlock', 'Name')).toBe('Hello Sim!')
})
test('ImprovedInstantMessage', () => {
@@ -289,15 +297,15 @@ describe('parseBody should work with buffer from createBody', () => {
const parsedMessage = parseBody(buffy.buffer)
expect(parsedMessage.name).toBe('ImprovedInstantMessage')
expect(parsedMessage.getValues('AgentData', ['AgentID', 'SessionID'])).toEqual({
expect(getValuesOf(parsedMessage, 'AgentData', ['AgentID', 'SessionID'])).toEqual({
AgentID: aUUID,
SessionID: aUUID
})
expect(parsedMessage.getStringValues('MessageBlock', ['FromAgentName', 'Message'])).toEqual({
expect(getStringValuesOf(parsedMessage, 'MessageBlock', ['FromAgentName', 'Message'])).toEqual({
FromAgentName: 'Testy MacTestface',
Message: 'Hello to my World of tests!'
})
expect(parsedMessage.getValues('MessageBlock', [
expect(getValuesOf(parsedMessage, 'MessageBlock', [
'FromGroup',
'ToAgentID',
'ParentEstateID',
+3
View File
@@ -72,6 +72,9 @@ export default function accountReducer (state = getDefault(), action) {
}
})
case 'SavingAvatar':
return state.set('sync', true)
case 'AvatarSaved':
return state.set('savedAvatars',
state.get('savedAvatars').push(
+8 -1
View File
@@ -5,6 +5,7 @@
import Immutable from 'immutable'
import AvatarName from '../avatarName'
import {mapBlockOf} from '../network/msgGetters'
// Only adds a Name to names if it is new or did change
function addName (state, uuid, name) {
@@ -51,7 +52,13 @@ function namesReducer (state = Immutable.Map(), action) {
return action.localChatHistory.reduce(addNameFromLocalChat, selfName)
case 'UUIDNameReply':
return action.msg.reduce((state, {firstName, lastName, id}) => {
return mapBlockOf(action, 'UUIDNameBlock', getValue => {
return {
firstName: getValue('FirstName', true),
lastName: getValue('LastName', true),
id: getValue('ID')
}
}).reduce((state, {firstName, lastName, id}) => {
return addName(state, id, firstName + ' ' + lastName)
}, state)
+9 -3
View File
@@ -1,5 +1,7 @@
import {Map} from 'immutable'
import {getValueOf, getValuesOf} from '../network/msgGetters'
export default function SessionReducer (state = Map({loggedIn: false, error: null}), action) {
switch (action.type) {
case 'didLogin':
@@ -47,14 +49,18 @@ export default function SessionReducer (state = Map({loggedIn: false, error: nul
case 'AgentMovementComplete':
return state.mergeDeep({
position: {
position: action.msg.position,
lookAt: action.msg.lookAt
position: getValueOf(action, 'Data', 'Position'),
lookAt: getValueOf(action, 'Data', 'LookAt')
}
})
case 'RegionInfo':
return state.mergeDeep({
regionInfo: Object.assign({}, action.msg.regionInfo, action.msg.regionInfo2)
regionInfo: Object.assign(
{},
getValuesOf(action, 'RegionInfo', 0, []),
getValuesOf(action, 'RegionInfo2', 0, [])
)
})
case 'RegionHandshake':