mirror of
https://github.com/Terreii/andromeda-viewer.git
synced 2026-08-14 09:01:59 +00:00
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user